"""Tests for the it placeholder projections."""

from __future__ import annotations

from dataclasses import dataclass

import pytest

from crystflux.v1 import Crystallizer, it


def test_it_get_projection_maps_strings() -> None:
    data = [{"name": "Alice"}, {"name": "Bob"}]
    names = Crystallizer.latent(data).map(it.get("name")).as_str_array()
    assert names == ("Alice", "Bob")


def test_it_attr_projection_sugar() -> None:
    data = [{"name": "Alice"}, {"name": "Bob"}]
    names = Crystallizer.latent(data).map(it.name).as_str_array()
    assert names == ("Alice", "Bob")


def test_it_index_projection_maps_first_elements() -> None:
    matrix = [[1, 2], [3, 4]]
    tops = Crystallizer.latent(matrix).map(it[0]).as_int_array()
    assert tops == (1, 3)


def test_it_chain_get_at_get() -> None:
    data = [{"names": [{"age": 30}]}, {"names": [{"age": 40}]}]
    ages = Crystallizer.latent(data).map(it.names[0].age).as_int_array()
    assert ages == (30, 40)


def test_it_filter_predicate_with_projection() -> None:
    data = [
        {"name": "Alice", "active": True},
        {"name": "Bob", "active": False},
        {"name": "Charlie", "active": True},
    ]
    users = Crystallizer.latent(data).to_array()
    active = users.filter(lambda x: it.active(x).as_bool() is True)
    names = active.map(it.name).as_str_array()
    assert names == ("Alice", "Charlie")


def test_it_filter_predicate_comparison_bool() -> None:
    data = [
        {"name": "Alice", "active": True},
        {"name": "Bob", "active": False},
        {"name": "Charlie"},
    ]
    users = Crystallizer.latent(data).to_array()
    active = users.filter(it.active.as_bool().is_true())
    names = active.map(it.name).as_str_array()
    assert names == ("Alice",)


def test_it_filter_predicate_comparison_int() -> None:
    data = [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 18},
        {"name": "Charlie"},
    ]
    users = Crystallizer.latent(data).to_array()
    adults = users.filter(it.age.as_int() > 20)
    names = adults.map(it.name).as_str_array()
    assert names == ("Alice",)


def test_it_filter_predicate_is_false() -> None:
    data = [
        {"name": "Alice", "active": True},
        {"name": "Bob", "active": False},
        {"name": "Charlie"},
    ]
    users = Crystallizer.latent(data).to_array()
    inactive = users.filter(it.active.as_bool().is_false())
    names = inactive.map(it.name).as_str_array()
    assert names == ("Bob",)


def test_it_filter_predicate_comparison_eq_ne() -> None:
    data = [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 18},
        {"name": "Charlie"},
    ]
    users = Crystallizer.latent(data).to_array()
    adults = users.filter(it.age.as_int() == 30)
    minors = users.filter(it.age.as_int() != 30)
    adult_names = adults.map(it.name).as_str_array()
    minor_names = minors.map(it.name).as_str_array()
    assert adult_names == ("Alice",)
    assert minor_names == ("Bob",)


def test_it_filter_predicate_comparison_eq_ne_methods() -> None:
    data = [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 18},
    ]
    users = Crystallizer.latent(data).to_array()
    adults = users.filter(it.age.as_int().eq(30))
    minors = users.filter(it.age.as_int().ne(30))
    adult_names = adults.map(it.name).as_str_array()
    minor_names = minors.map(it.name).as_str_array()
    assert adult_names == ("Alice",)
    assert minor_names == ("Bob",)


def test_it_filter_predicate_comparison_lt_le_gt_ge() -> None:
    data = [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 18},
        {"name": "Charlie", "age": 25},
        {"name": "Dana"},
    ]
    users = Crystallizer.latent(data).to_array()

    lt_25 = users.filter(it.age.as_int() < 25).map(it.name).as_str_array()
    le_25 = users.filter(it.age.as_int() <= 25).map(it.name).as_str_array()
    gt_25 = users.filter(it.age.as_int() > 25).map(it.name).as_str_array()
    ge_25 = users.filter(it.age.as_int() >= 25).map(it.name).as_str_array()

    assert lt_25 == ("Bob",)
    assert le_25 == ("Bob", "Charlie")
    assert gt_25 == ("Alice",)
    assert ge_25 == ("Alice", "Charlie")


def test_it_filter_predicate_comparison_methods_lt_le_gt_ge() -> None:
    data = [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 18},
        {"name": "Charlie", "age": 25},
        {"name": "Dana"},
    ]
    users = Crystallizer.latent(data).to_array()

    lt_25 = users.filter(it.age.as_int().lt(25)).map(it.name).as_str_array()
    le_25 = users.filter(it.age.as_int().le(25)).map(it.name).as_str_array()
    gt_25 = users.filter(it.age.as_int().gt(25)).map(it.name).as_str_array()
    ge_25 = users.filter(it.age.as_int().ge(25)).map(it.name).as_str_array()

    assert lt_25 == ("Bob",)
    assert le_25 == ("Bob", "Charlie")
    assert gt_25 == ("Alice",)
    assert ge_25 == ("Alice", "Charlie")


def test_it_filter_predicate_expected_int_success() -> None:
    data = [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 18},
    ]
    users = Crystallizer.latent(data).to_array()
    adults = users.filter(it.age.expected_int() > 20)
    names = adults.map(it.name).as_str_array()
    assert names == ("Alice",)


def test_it_filter_predicate_expected_bool_raises_on_missing() -> None:
    data = [
        {"name": "Alice", "active": True},
        {"name": "Bob"},
    ]
    users = Crystallizer.latent(data).to_array()
    with pytest.raises(TypeError):
        users.filter(it.active.expected_bool().is_true())


def test_it_filter_predicate_expected_bool_is_true() -> None:
    data = [
        {"name": "Alice", "active": True},
        {"name": "Bob", "active": False},
    ]
    users = Crystallizer.latent(data).to_array()
    active = users.filter(it.active.expected_bool().is_true())
    names = active.map(it.name).as_str_array()
    assert names == ("Alice",)


def test_it_filter_predicate_expected_comparison_eq_ne() -> None:
    data = [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 18},
    ]
    users = Crystallizer.latent(data).to_array()
    adults = users.filter(it.age.expected_int() == 30)
    minors = users.filter(it.age.expected_int() != 30)
    adult_names = adults.map(it.name).as_str_array()
    minor_names = minors.map(it.name).as_str_array()
    assert adult_names == ("Alice",)
    assert minor_names == ("Bob",)


def test_it_filter_predicate_expected_comparison_lt_le_gt_ge() -> None:
    data = [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 18},
        {"name": "Charlie", "age": 25},
    ]
    users = Crystallizer.latent(data).to_array()

    lt_25 = users.filter(it.age.expected_int() < 25).map(it.name).as_str_array()
    le_25 = users.filter(it.age.expected_int() <= 25).map(it.name).as_str_array()
    gt_25 = users.filter(it.age.expected_int() > 25).map(it.name).as_str_array()
    ge_25 = users.filter(it.age.expected_int() >= 25).map(it.name).as_str_array()

    assert lt_25 == ("Bob",)
    assert le_25 == ("Bob", "Charlie")
    assert gt_25 == ("Alice",)
    assert ge_25 == ("Alice", "Charlie")


def test_it_filter_predicate_expected_comparison_methods_lt_le_gt_ge() -> None:
    data = [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 18},
        {"name": "Charlie", "age": 25},
    ]
    users = Crystallizer.latent(data).to_array()

    lt_25 = users.filter(it.age.expected_int().lt(25)).map(it.name).as_str_array()
    le_25 = users.filter(it.age.expected_int().le(25)).map(it.name).as_str_array()
    gt_25 = users.filter(it.age.expected_int().gt(25)).map(it.name).as_str_array()
    ge_25 = users.filter(it.age.expected_int().ge(25)).map(it.name).as_str_array()

    assert lt_25 == ("Bob",)
    assert le_25 == ("Bob", "Charlie")
    assert gt_25 == ("Alice",)
    assert ge_25 == ("Alice", "Charlie")


def test_it_reserved_name_requires_explicit_get() -> None:
    # Keys that collide with placeholder methods (e.g., "get") must use it.get("get").
    data = [{"get": "ok"}, {"get": "no"}]
    values = Crystallizer.latent(data).map(it.get("get")).as_str_array()
    assert values == ("ok", "no")


def test_it_reserved_name_sugar_fails() -> None:
    # it.get is a method, so it cannot be used as sugar for the "get" key.
    data = [{"get": "ok"}, {"get": "no"}]
    with pytest.raises(TypeError):
        Crystallizer.latent(data).map(it.get).as_str_array()  # type: ignore


def test_it_projection_is_true_is_false() -> None:
    """Direct is_true() and is_false() on Projection."""
    data = [
        {"name": "Alice", "active": True},
        {"name": "Bob", "active": False},
        {"name": "Charlie", "active": True},
    ]
    users = Crystallizer.latent(data).to_array()

    # is_true() - direct on Projection (sugar for as_bool().is_true())
    active = users.filter(it.active.is_true())
    assert active.map(it.name).as_str_array() == ("Alice", "Charlie")

    # is_false() - direct on Projection (sugar for as_bool().is_false())
    inactive = users.filter(it.active.is_false())
    assert inactive.map(it.name).as_str_array() == ("Bob",)


# ----------------------------------------------------------------------
# Edge case tests for improved coverage
# ----------------------------------------------------------------------


def test_mixed_type_comparisons() -> None:
    """Test comparing int with float, and other mixed types."""
    data = [{"name": "Alice", "age": 30, "score": 85.5}, {"name": "Bob", "age": 25, "score": 90.0}]
    users = Crystallizer.latent(data).to_array()

    # int vs int comparison
    assert users.filter(it.age > 25).map(it.name).as_str_array() == ("Alice",)

    # float vs float comparison (both Alice and Bob have score > 85.0)
    assert users.filter(it.score > 85.0).map(it.name).as_str_array() == ("Alice", "Bob")

    # int vs float comparison (should work)
    filtered = users.filter(it.age.as_int() > 25.5)  # int > float
    assert filtered.map(it.name).as_str_array() == ("Alice",)


def test_deep_nesting_projections() -> None:
    """Test projections with 10+ levels of nesting."""
    data = [
        {
            "level1": {
                "level2": {
                    "level3": {
                        "level4": {
                            "level5": {
                                "level6": {"level7": {"level8": {"level9": {"level10": "deep"}}}}
                            }
                        }
                    }
                }
            }
        }
    ]
    # Use map with it projection to access deeply nested field
    result = (
        Crystallizer.latent(data)
        .map(it.level1.level2.level3.level4.level5.level6.level7.level8.level9.level10)
        .as_str_array()
    )
    assert result == ("deep",)


def test_none_propagation_through_chains() -> None:
    """Test None values propagate correctly through complex chains."""
    data = [
        {"user": {"profile": {"age": 30}}},
        {"user": {"profile": None}},  # Missing profile
        {"user": None},  # Missing user
    ]
    users = Crystallizer.latent(data).to_array()

    # Only first user should have valid age
    ages = users.map(it.user.profile.age).as_int_array()
    assert ages == (30,)


def test_projection_with_mixed_types() -> None:
    """Test projections where parent type changes."""
    data = [{"value": 42}, {"value": "string"}, {"value": True}]
    items = Crystallizer.latent(data).to_array()

    # Should handle mixed types gracefully
    numbers = items.filter(it.value.as_int() > 0).map(it.value).as_int_array()
    assert numbers == (42,)  # Only 42


def test_it_filter_predicate_expected_float_comparison_lt_le_gt_ge() -> None:
    """Test expected_float with all 6 comparison operators (symmetric to expected_int)."""
    data = [
        {"name": "Alice", "score": 85.5},
        {"name": "Bob", "score": 70.0},
        {"name": "Charlie", "score": 92.0},
    ]
    users = Crystallizer.latent(data).to_array()

    gt_85 = users.filter(it.score.expected_float() > 85.0).map(it.name).as_str_array()
    le_85 = users.filter(it.score.expected_float() <= 85.0).map(it.name).as_str_array()
    lt_85 = users.filter(it.score.expected_float() < 85.0).map(it.name).as_str_array()
    ge_85 = users.filter(it.score.expected_float() >= 85.0).map(it.name).as_str_array()
    eq_85 = users.filter(it.score.expected_float() == 85.5).map(it.name).as_str_array()
    ne_85 = users.filter(it.score.expected_float() != 85.5).map(it.name).as_str_array()

    assert gt_85 == ("Alice", "Charlie")
    assert le_85 == ("Bob",)
    assert lt_85 == ("Bob",)
    assert ge_85 == ("Alice", "Charlie")
    assert eq_85 == ("Alice",)
    assert ne_85 == (
        "Bob",
        "Charlie",
    )


def test_it_filter_predicate_expected_float_comparison_methods_lt_le_gt_ge() -> None:
    """Test expected_float with all 6 comparison methods (symmetric to expected_int)."""
    data = [
        {"name": "Alice", "score": 85.5},
        {"name": "Bob", "score": 70.0},
        {"name": "Charlie", "score": 92.0},
    ]
    users = Crystallizer.latent(data).to_array()

    lt_85 = users.filter(it.score.expected_float().lt(85.0)).map(it.name).as_str_array()
    le_85 = users.filter(it.score.expected_float().le(85.0)).map(it.name).as_str_array()
    gt_85 = users.filter(it.score.expected_float().gt(85.0)).map(it.name).as_str_array()
    ge_85 = users.filter(it.score.expected_float().ge(85.0)).map(it.name).as_str_array()
    eq_85 = users.filter(it.score.expected_float().eq(85.5)).map(it.name).as_str_array()
    ne_85 = users.filter(it.score.expected_float().ne(85.5)).map(it.name).as_str_array()

    assert lt_85 == ("Bob",)
    assert le_85 == ("Bob",)
    assert gt_85 == ("Alice", "Charlie")
    assert ge_85 == ("Alice", "Charlie")
    assert eq_85 == ("Alice",)
    assert ne_85 == (
        "Bob",
        "Charlie",
    )


def test_it_filter_predicate_expected_float_raises_on_missing() -> None:
    """Test expected_float raises TypeError on missing values (symmetric to expected_bool)."""
    data = [
        {"name": "Alice", "score": 85.5},
        {"name": "Bob"},
    ]
    users = Crystallizer.latent(data).to_array()

    with pytest.raises(TypeError):
        users.filter(it.score.expected_float() > 80.0)


def test_it_at_explicit_usage() -> None:
    """Test explicit it.at() usage for accessing array elements."""
    matrix = [[1, 2, 3], [4, 5, 6]]
    val = Crystallizer.latent(matrix).to_array()

    # Explicit at() method call
    first_elements = val.map(it.at(0)).as_int_array()
    assert first_elements == (1, 4)

    # Compare with index syntax for equivalence
    index_syntax = val.map(it[0]).as_int_array()
    assert first_elements == index_syntax


def test_projection_negative_index() -> None:
    """Test negative index access (it[-1], it[-2]) in filter/map contexts."""
    data = [
        {"items": ["a", "b", "c"]},
        {"items": ["x", "y"]},
    ]
    val = Crystallizer.latent(data).to_array()

    # In latent mode, negative indices return None (VOID behavior)
    # None values are skipped in as_str_array()
    items_arr = val.map(it.items)
    last_items = items_arr.map(it[-1]).as_str_array()
    assert last_items == ()

    second_last = items_arr.map(it[-2]).as_str_array()
    assert second_last == ()

    # In strict mode, negative indices should raise IndexError
    strict_val = Crystallizer.strict(data).to_array()
    with pytest.raises(IndexError, match="Array index out of range"):
        strict_items = strict_val.map(it.items)
        strict_items.map(it[-1])


# ----------------------------------------------------------------------
# Additional edge case tests for coverage (L44, L60)
# ----------------------------------------------------------------------


def test_it_dunder_methods_raise_attribute_error() -> None:
    """Test that custom dunder attributes raise AttributeError (L44, L170)."""
    # Projection class: accessing custom dunder should raise AttributeError
    from crystflux.v1.core.value_projections import Projection

    proj = Projection()
    with pytest.raises(AttributeError, match="__custom_dunder__"):
        proj.__custom_dunder__

    # It class: accessing custom dunder should also raise AttributeError
    with pytest.raises(AttributeError, match="__another_dunder__"):
        it.__another_dunder__


def test_it_invalid_op_raises_value_error() -> None:
    """Test that invalid op tags raise ValueError (L60)."""
    from crystflux.v1.core.value_projections import Op, Projection

    # Create a projection with an invalid op tag (bypassing type checking)
    invalid_op = Op("invalid", "test")  # type: ignore[arg-type]
    projection = Projection((invalid_op,))

    val = Crystallizer.latent({"a": 1})

    # Calling projection should raise ValueError
    with pytest.raises(ValueError, match="Unsupported op tag: invalid"):
        projection(val)


def test_it_comparison_with_none_on_rhs() -> None:
    """Test that comparing with None on RHS triggers L104 default to int."""
    data = [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 25},
        {"name": "Charlie", "age": None},  # None value on LHS
    ]
    users = Crystallizer.latent(data).to_array()

    # RHS is None, triggers _infer_scalar_projection(None) -> as_int()
    # This covers L104 where None defaults to int type
    result = users.filter(it.age > None)

    # Comparing with None should result in empty array
    assert result.as_json_value() == ()

    # Also test other comparison operators to ensure L104 coverage
    assert users.filter(it.age < None).as_json_value() == ()
    assert users.filter(it.age >= None).as_json_value() == ()
    assert users.filter(it.age <= None).as_json_value() == ()
    assert users.filter(it.age == None).as_json_value() == ()  # noqa: E711
    assert users.filter(it.age != None).map(it.name).as_str_array() == (  # noqa: E711
        "Alice",
        "Bob",
    )


def test_incomplete_scalar_projection_predicate_raises_not_implemented() -> None:
    """Test that scalar projection without _predicate override raises NotImplementedError (L256).

    This test creates a scalar projection class that inherits from _BaseScalarProjection
    but does NOT override _predicate(), ensuring that base class NotImplementedError
    is triggered when comparison operators are used.
    """
    from crystflux.v1.core.value_projections import (
        _BaseScalarProjection, # type: ignore[misc]
        Projection,
    )

    # Create a custom scalar projection class that DOES NOT override _predicate()
    @dataclass(frozen=True, eq=False)
    class IncompleteScalarProjection(_BaseScalarProjection):
        """Scalar projection without _predicate override for testing base error path."""

        # _predicate() is intentionally NOT implemented here

    proj = Projection()
    incomplete_proj = IncompleteScalarProjection(proj, scalar_type="int")

    # Calling comparison operators should trigger base class _predicate() and raise NotImplementedError
    with pytest.raises(NotImplementedError):
        incomplete_proj == 1 # type: ignore[misc]

    with pytest.raises(NotImplementedError):
        incomplete_proj != 1 # type: ignore[misc]

    with pytest.raises(NotImplementedError):
        incomplete_proj < 1  # type: ignore[misc]
