"""Tests for optimizer functionality."""

from therismos import FALSE, TRUE, AllExpr, AnyExpr, Eq, F, In, NotExpr, optimize


class TestBasicLogicalSimplifications:
    """Test basic logical simplification rules."""

    def test_all_expr_with_false(self) -> None:
        """Test AllExpr([..., FALSE, ...]) → FALSE."""
        age = F("age")
        expr = AllExpr(age > 18, FALSE, age < 65)
        result, records = optimize(expr)
        assert result == FALSE
        assert len(records) > 0
        assert any("FALSE" in r.reason for r in records)

    def test_any_expr_with_true(self) -> None:
        """Test AnyExpr([..., TRUE, ...]) → TRUE."""
        age = F("age")
        expr = AnyExpr(age > 18, TRUE, age < 65)
        result, records = optimize(expr)
        assert result == TRUE
        assert len(records) > 0
        assert any("TRUE" in r.reason for r in records)

    def test_empty_all_expr(self) -> None:
        """Test AllExpr([]) → TRUE."""
        expr = AllExpr()
        result, _ = optimize(expr)
        assert result == TRUE

    def test_empty_any_expr(self) -> None:
        """Test AnyExpr([]) → FALSE."""
        expr = AnyExpr()
        result, _ = optimize(expr)
        assert result == FALSE

    def test_single_operand_all_expr(self) -> None:
        """Test AllExpr([x]) → x."""
        age = F("age")
        expr = AllExpr(age > 18)
        result, _ = optimize(expr)
        assert result == age > 18

    def test_single_operand_any_expr(self) -> None:
        """Test AnyExpr([x]) → x."""
        age = F("age")
        expr = AnyExpr(age > 18)
        result, _ = optimize(expr)
        assert result == age > 18

    def test_identity_elimination_all_expr(self) -> None:
        """Test elimination of TRUE from AllExpr."""
        age = F("age")
        name = F("name")
        expr = AllExpr(age > 18, TRUE, name == "Alice")
        result, _ = optimize(expr)
        assert isinstance(result, AllExpr)
        assert len(result.exprs) == 2
        assert all(not isinstance(e, type(TRUE)) for e in result.exprs)

    def test_identity_elimination_any_expr(self) -> None:
        """Test elimination of FALSE from AnyExpr."""
        age = F("age")
        name = F("name")
        expr = AnyExpr(age > 18, FALSE, name == "Alice")
        result, _ = optimize(expr)
        assert isinstance(result, AnyExpr)
        assert len(result.exprs) == 2
        assert all(not isinstance(e, type(FALSE)) for e in result.exprs)


class TestNotSimplification:
    """Test NOT expression simplification rules."""

    def test_not_true(self) -> None:
        """Test NOT(TRUE) → FALSE."""
        expr = NotExpr(TRUE)
        result, records = optimize(expr)
        assert result == FALSE
        assert any("NOT(TRUE)" in r.reason for r in records)

    def test_not_false(self) -> None:
        """Test NOT(FALSE) → TRUE."""
        expr = NotExpr(FALSE)
        result, records = optimize(expr)
        assert result == TRUE
        assert any("NOT(FALSE)" in r.reason for r in records)

    def test_double_negation(self) -> None:
        """Test NOT(NOT(x)) → x."""
        age = F("age")
        expr = NotExpr(NotExpr(age > 18))
        result, records = optimize(expr)
        assert result == age > 18
        assert any("NOT(NOT(x))" in r.reason for r in records)

    def test_de_morgan_not_and(self) -> None:
        """Test NOT(AllExpr(a, b)) → AnyExpr(NOT(a), NOT(b))."""
        age = F("age")
        name = F("name")
        expr = NotExpr(AllExpr(age > 18, name == "Alice"))
        result, _ = optimize(expr)
        assert isinstance(result, AnyExpr)
        assert len(result.exprs) == 2
        assert all(isinstance(e, NotExpr) for e in result.exprs)

    def test_de_morgan_not_or(self) -> None:
        """Test NOT(AnyExpr(a, b)) → AllExpr(NOT(a), NOT(b))."""
        age = F("age")
        name = F("name")
        expr = NotExpr(AnyExpr(age > 18, name == "Alice"))
        result, _ = optimize(expr)
        assert isinstance(result, AllExpr)
        assert len(result.exprs) == 2
        assert all(isinstance(e, NotExpr) for e in result.exprs)


class TestContradictionDetection:
    """Test contradiction detection in AND expressions."""

    def test_eq_ne_contradiction(self) -> None:
        """Test (x == v) AND (x != v) → FALSE."""
        age = F("age")
        expr = AllExpr(age == 18, age != 18)
        result, records = optimize(expr)
        assert result == FALSE
        assert any("Contradiction" in r.reason for r in records)

    def test_is_null_is_not_null_contradiction(self) -> None:
        """Test is_null(x) AND is_not_null(x) → FALSE."""
        deleted = F("deleted_at")
        expr = AllExpr(deleted.is_null(), deleted.is_not_null())
        result, _ = optimize(expr)
        assert result == FALSE

    def test_lt_gt_contradiction(self) -> None:
        """Test (x < a) AND (x > b) where b >= a → FALSE."""
        age = F("age")
        expr = AllExpr(age < 30, age > 40)
        result, _ = optimize(expr)
        assert result == FALSE

    def test_le_gt_contradiction(self) -> None:
        """Test (x <= a) AND (x > a) → FALSE."""
        age = F("age")
        expr = AllExpr(age <= 30, age > 30)
        result, _ = optimize(expr)
        assert result == FALSE

    def test_ge_lt_contradiction(self) -> None:
        """Test (x >= b) AND (x < b) → FALSE."""
        age = F("age")
        expr = AllExpr(age >= 30, age < 30)
        result, _ = optimize(expr)
        assert result == FALSE

    def test_no_contradiction_non_overlapping_ranges(self) -> None:
        """Test that non-contradictory ranges are preserved."""
        age = F("age")
        expr = AllExpr(age > 18, age < 65)
        result, _ = optimize(expr)
        assert isinstance(result, AllExpr)
        assert len(result.exprs) == 2


class TestTautologyDetection:
    """Test tautology detection in OR expressions."""

    def test_eq_ne_tautology(self) -> None:
        """Test (x == v) OR (x != v) → TRUE."""
        age = F("age")
        expr = AnyExpr(age == 18, age != 18)
        result, records = optimize(expr)
        assert result == TRUE
        assert any("Tautology" in r.reason for r in records)

    def test_is_null_is_not_null_tautology(self) -> None:
        """Test is_null(x) OR is_not_null(x) → TRUE."""
        deleted = F("deleted_at")
        expr = AnyExpr(deleted.is_null(), deleted.is_not_null())
        result, _ = optimize(expr)
        assert result == TRUE

    def test_lt_ge_tautology(self) -> None:
        """Test (x < v) OR (x >= v) → TRUE."""
        age = F("age")
        expr = AnyExpr(age < 30, age >= 30)
        result, _ = optimize(expr)
        assert result == TRUE

    def test_le_gt_tautology(self) -> None:
        """Test (x <= v) OR (x > v) → TRUE."""
        age = F("age")
        expr = AnyExpr(age <= 30, age > 30)
        result, _ = optimize(expr)
        assert result == TRUE


class TestOrEqualityAggregation:
    """Test aggregation of OR equality chains to IN."""

    def test_simple_or_equality_aggregation(self) -> None:
        """Test (f == v1) OR (f == v2) → f IN (v1, v2)."""
        status = F("status")
        expr = AnyExpr(status == "active", status == "pending")
        result, records = optimize(expr)
        assert isinstance(result, In)
        assert set(result.values) == {"active", "pending"}
        assert any("IN" in r.reason for r in records)

    def test_multiple_equality_aggregation(self) -> None:
        """Test aggregation of multiple equalities."""
        status = F("status")
        expr = AnyExpr(status == "active", status == "pending", status == "completed")
        result, _ = optimize(expr)
        assert isinstance(result, In)
        assert set(result.values) == {"active", "pending", "completed"}

    def test_eq_or_in_aggregation(self) -> None:
        """Test (f == v1) OR (f IN (v2, v3)) → f IN (v1, v2, v3)."""
        status = F("status")
        expr = AnyExpr(status == "active", status.is_in("pending", "completed"))
        result, records = optimize(expr)
        assert isinstance(result, In)
        assert set(result.values) == {"active", "pending", "completed"}
        assert any("IN" in r.reason for r in records)

    def test_in_or_eq_aggregation(self) -> None:
        """Test (f IN (v1, v2)) OR (f == v3) → f IN (v1, v2, v3)."""
        status = F("status")
        expr = AnyExpr(status.is_in("active", "pending"), status == "completed")
        result, _ = optimize(expr)
        assert isinstance(result, In)
        assert set(result.values) == {"active", "pending", "completed"}

    def test_multiple_in_aggregation(self) -> None:
        """Test (f IN (v1, v2)) OR (f IN (v3, v4)) → f IN (v1, v2, v3, v4)."""
        status = F("status")
        expr = AnyExpr(status.is_in("active", "pending"), status.is_in("completed", "archived"))
        result, _ = optimize(expr)
        assert isinstance(result, In)
        assert set(result.values) == {"active", "pending", "completed", "archived"}

    def test_mixed_eq_and_in_aggregation(self) -> None:
        """Test (f == v1) OR (f IN (v2, v3)) OR (f == v4) → f IN (v1, v2, v3, v4)."""
        status = F("status")
        expr = AnyExpr(
            status == "active",
            status.is_in("pending", "completed"),
            status == "archived",
        )
        result, _ = optimize(expr)
        assert isinstance(result, In)
        assert set(result.values) == {"active", "pending", "completed", "archived"}

    def test_mixed_expressions_no_aggregation(self) -> None:
        """Test that mixed expressions (not all equalities/IN) don't aggregate."""
        status = F("status")
        age = F("age")
        expr = AnyExpr(status == "active", age > 18)
        result, _ = optimize(expr)
        # Should remain as AnyExpr since not all are equalities on same field
        assert isinstance(result, AnyExpr)

    def test_partial_aggregation(self) -> None:
        """Test aggregation with other expressions present."""
        status = F("status")
        age = F("age")
        expr = AnyExpr(status == "active", status == "pending", age > 18)
        result, _ = optimize(expr)
        # Status equalities should be aggregated, age expression preserved
        assert isinstance(result, AnyExpr)
        # Should have In expression and age comparison
        has_in = any(isinstance(e, In) for e in result.exprs)
        assert has_in

    def test_partial_aggregation_with_in(self) -> None:
        """Test aggregation with IN and other expressions present."""
        status = F("status")
        age = F("age")
        expr = AnyExpr(status == "active", status.is_in("pending", "completed"), age > 18)
        result, _ = optimize(expr)
        # Status expressions should be aggregated, age expression preserved
        assert isinstance(result, AnyExpr)
        assert len(result.exprs) == 2
        # Should have In expression and age comparison
        has_in = any(isinstance(e, In) for e in result.exprs)
        assert has_in
        in_expr = next(e for e in result.exprs if isinstance(e, In))
        assert set(in_expr.values) == {"active", "pending", "completed"}

    def test_different_fields_no_cross_aggregation(self) -> None:
        """Test that equalities on different fields don't aggregate together."""
        status = F("status")
        role = F("role")
        expr = AnyExpr(status == "active", role == "admin")
        result, _ = optimize(expr)
        # Different fields, should not aggregate
        assert isinstance(result, AnyExpr)
        assert not any(isinstance(e, In) for e in result.exprs)


class TestAndEqualityIntersection:
    """Test intersection of AND equality/IN chains."""

    def test_eq_and_eq_same_value(self) -> None:
        """Test (f == v) AND (f == v) → f == v."""
        status = F("status")
        expr = AllExpr(status == "active", status == "active")
        result, _ = optimize(expr)
        assert result == status == "active"

    def test_eq_and_eq_different_values(self) -> None:
        """Test (f == v1) AND (f == v2) → FALSE when v1 != v2."""
        status = F("status")
        expr = AllExpr(status == "active", status == "pending")
        result, records = optimize(expr)
        assert result == FALSE
        assert any("Contradiction" in r.reason for r in records)

    def test_eq_and_in_value_in_set(self) -> None:
        """Test (f == v) AND (f IN (v, ...)) → f == v."""
        status = F("status")
        expr = AllExpr(status == "active", status.is_in("active", "pending", "completed"))
        result, records = optimize(expr)
        assert result == status == "active"
        assert any("AND" in r.reason and "IN" in r.reason for r in records)

    def test_eq_and_in_value_not_in_set(self) -> None:
        """Test (f == v) AND (f IN (...)) → FALSE when v not in set."""
        status = F("status")
        expr = AllExpr(status == "archived", status.is_in("active", "pending", "completed"))
        result, records = optimize(expr)
        assert result == FALSE
        assert any("Contradiction" in r.reason for r in records)

    def test_in_and_eq_value_in_set(self) -> None:
        """Test (f IN (v, ...)) AND (f == v) → f == v."""
        status = F("status")
        expr = AllExpr(status.is_in("active", "pending", "completed"), status == "pending")
        result, _ = optimize(expr)
        assert result == status == "pending"

    def test_multiple_in_intersection_empty(self) -> None:
        """Test (f IN (v1, v2)) AND (f IN (v3, v4)) → FALSE when no overlap."""
        status = F("status")
        expr = AllExpr(status.is_in("active", "pending"), status.is_in("completed", "archived"))
        result, records = optimize(expr)
        assert result == FALSE
        assert any("Contradiction" in r.reason for r in records)

    def test_multiple_in_intersection_single(self) -> None:
        """Test (f IN (v1, v2)) AND (f IN (v2, v3)) → f == v2 when intersection is single."""
        status = F("status")
        expr = AllExpr(status.is_in("active", "pending"), status.is_in("pending", "completed"))
        result, records = optimize(expr)
        assert result == status == "pending"
        assert any("AND" in r.reason and "IN" in r.reason for r in records)

    def test_multiple_in_intersection_multiple(self) -> None:
        """Test (f IN (v1, v2, v3)) AND (f IN (v2, v3, v4)) → f IN (v2, v3)."""
        status = F("status")
        expr = AllExpr(
            status.is_in("active", "pending", "completed"),
            status.is_in("pending", "completed", "archived"),
        )
        result, records = optimize(expr)
        assert isinstance(result, In)
        assert set(result.values) == {"pending", "completed"}
        assert any("AND" in r.reason and "IN" in r.reason for r in records)

    def test_mixed_eq_and_in_intersection(self) -> None:
        """Test (f == v1) AND (f IN (v1, v2)) AND (f IN (v1, v3)) → f == v1."""
        status = F("status")
        expr = AllExpr(
            status == "active",
            status.is_in("active", "pending"),
            status.is_in("active", "completed"),
        )
        result, _ = optimize(expr)
        assert result == status == "active"

    def test_partial_intersection_with_other_fields(self) -> None:
        """Test intersection with other field expressions present."""
        status = F("status")
        age = F("age")
        expr = AllExpr(
            status.is_in("active", "pending"),
            status.is_in("pending", "completed"),
            age > 18,
        )
        result, _ = optimize(expr)
        # Status should be reduced to single value, age expression preserved
        assert isinstance(result, AllExpr)
        assert len(result.exprs) == 2
        # One should be the status == "pending", the other age > 18
        has_status_eq = any(isinstance(e, Eq) and e.field.name == "status" for e in result.exprs)
        assert has_status_eq

    def test_different_fields_no_cross_intersection(self) -> None:
        """Test that IN/Eq on different fields don't intersect."""
        status = F("status")
        role = F("role")
        expr = AllExpr(status.is_in("active", "pending"), role.is_in("admin", "user"))
        result, _ = optimize(expr)
        # Should remain as AllExpr with both IN expressions
        assert isinstance(result, AllExpr)
        assert len(result.exprs) == 2
        assert all(isinstance(e, In) for e in result.exprs)


class TestOptimizationTracking:
    """Test optimization tracking functionality."""

    def test_track_enabled(self) -> None:
        """Test that tracking is enabled by default."""
        expr = NotExpr(TRUE)
        result, records = optimize(expr)
        assert result == FALSE
        assert len(records) > 0
        assert all(hasattr(r, "before") for r in records)
        assert all(hasattr(r, "after") for r in records)
        assert all(hasattr(r, "reason") for r in records)

    def test_records_collecting_parameter(self) -> None:
        """Test that records can be collected into a provided list."""
        expr = NotExpr(TRUE)
        my_records: list = []
        result, records = optimize(expr, my_records)
        assert result == FALSE
        # Should return the same list we passed in
        assert records is my_records
        assert len(my_records) > 0

    def test_multiple_optimizations_tracked(self) -> None:
        """Test that multiple optimization steps are tracked."""
        # NOT(NOT(TRUE)) should have multiple optimization steps
        # NOT(NOT(TRUE)) → NOT(FALSE) → TRUE
        expr = NotExpr(NotExpr(TRUE))
        result, records = optimize(expr)
        assert result == TRUE
        # Should have at least 2 optimization steps
        assert len(records) >= 2

    def test_optimization_record_structure(self) -> None:
        """Test OptimizationRecord structure."""
        expr = NotExpr(TRUE)
        result, records = optimize(expr)
        assert len(records) > 0
        record = records[0]
        assert record.before == NotExpr(TRUE)
        assert record.after == FALSE
        assert isinstance(record.reason, str)
        assert len(record.reason) > 0

    def test_child_optimization_recorded(self) -> None:
        """Test that child optimization steps are recorded."""
        age = F("age")
        status = F("status")
        # Create nested expression where inner part will be optimized
        inner_contradiction = AllExpr(age >= 25, age < 25)
        outer = AllExpr(inner_contradiction, status == "active")

        result, records = optimize(outer)
        assert result == FALSE
        # Should record multiple steps:
        # 1. Child optimization (detecting contradiction)
        # 2. Possibly reconstruction with optimized children
        assert len(records) >= 1
        assert any("Contradiction" in r.reason for r in records)

    def test_all_expr_with_false_child_recorded(self) -> None:
        """Test that AllExpr with FALSE child is properly recorded."""
        age = F("age")
        # Manually construct AllExpr with FALSE
        expr = AllExpr(FALSE, age > 18)
        result, records = optimize(expr)
        assert result == FALSE
        assert len(records) == 1
        assert "FALSE" in records[0].reason

    def test_reason_no_trailing_newline(self) -> None:
        """Test that reason messages have no trailing newlines."""
        expr = NotExpr(TRUE)
        result, records = optimize(expr)
        for record in records:
            assert not record.reason.endswith("\n")
            assert not record.reason.endswith("\r")


class TestNestedOptimization:
    """Test optimization of deeply nested expressions."""

    def test_nested_not_optimization(self) -> None:
        """Test optimization of nested NOT expressions."""
        age = F("age")
        # NOT(NOT(NOT(NOT(age > 18)))) should simplify to age > 18
        expr = NotExpr(NotExpr(NotExpr(NotExpr(age > 18))))
        result, _ = optimize(expr)
        assert result == age > 18

    def test_nested_compound_optimization(self) -> None:
        """Test optimization of nested compound expressions."""
        age = F("age")
        # AllExpr(TRUE, AnyExpr(age > 18, FALSE)) should simplify
        expr = AllExpr(TRUE, AnyExpr(age > 18, FALSE))
        result, _ = optimize(expr)
        assert result == age > 18

    def test_complex_nested_optimization(self) -> None:
        """Test complex nested expression optimization."""
        age = F("age")
        status = F("status")
        # NOT(AllExpr(NOT(age > 18), status == "active"))
        # Should apply De Morgan: AnyExpr(NOT(NOT(age > 18)), NOT(status == "active"))
        # Then simplify NOT(NOT(age > 18)) to age > 18
        expr = NotExpr(AllExpr(NotExpr(age > 18), status == "active"))
        result, _ = optimize(expr)
        assert isinstance(result, AnyExpr)
        # First element should be age > 18 (double negation eliminated)
        assert result.exprs[0] == age > 18


class TestInSimplification:
    """Test simplification of IN expressions."""

    def test_in_single_value_to_eq(self) -> None:
        """Test f IN (v) → f == v."""
        status = F("status")
        expr = status.is_in("active")
        result, records = optimize(expr)
        assert result == status == "active"
        assert any("IN" in r.reason and "single" in r.reason.lower() for r in records)

    def test_in_multiple_values_unchanged(self) -> None:
        """Test that IN with multiple values is not modified."""
        status = F("status")
        expr = status.is_in("active", "pending")
        result, records = optimize(expr)
        assert isinstance(result, In)
        assert len(result.values) == 2
        assert len(records) == 0

    def test_in_empty_to_false(self) -> None:
        """Test f IN () → FALSE."""
        status = F("status")
        expr = In(status, ())
        result, records = optimize(expr)
        assert result == FALSE
        assert any("IN" in r.reason and "empty" in r.reason.lower() for r in records)


class TestNoOptimizationCases:
    """Test cases where no optimization should occur."""

    def test_simple_comparison_unchanged(self) -> None:
        """Test that simple comparisons are not modified."""
        age = F("age")
        expr = age > 18
        result, records = optimize(expr)
        assert result == expr
        assert len(records) == 0

    def test_valid_all_expr_unchanged(self) -> None:
        """Test that valid AllExpr without optimization opportunities is unchanged."""
        age = F("age")
        name = F("name")
        expr = AllExpr(age > 18, name == "Alice")
        result, records = optimize(expr)
        # Structure should be preserved
        assert isinstance(result, AllExpr)
        assert len(result.exprs) == 2
        # No optimization should have occurred
        assert len(records) == 0

    def test_valid_any_expr_unchanged(self) -> None:
        """Test that valid AnyExpr without optimization opportunities is unchanged."""
        status = F("status")
        expr = AnyExpr(status == "active", status != "active")
        # This is a tautology, so it WILL be optimized
        result, _ = optimize(expr)
        assert result == TRUE

        # Try a non-tautology
        role = F("role")
        expr2 = AnyExpr(status == "active", role == "admin")
        result2, records2 = optimize(expr2)
        assert isinstance(result2, AnyExpr)
        assert len(records2) == 0
