"""Tests for array length operations with recommended patterns."""

from typing import Any

import pytest

from crystflux.v1 import Crystallizer
from crystflux.v1.adapters import enable_dream_mode


@pytest.fixture
def simple_test_data() -> dict[str, Any]:
    """Simple test data for array length tests (no file I/O needed)."""
    return {
        # Basic arrays
        "str_array": ["apple", "banana", "cherry"],
        "int_array": [1, 2, 3, 4, 5],
        "float_array": [1.1, 2.2, 3.3],
        "scalars_array": ["hello", 42, True, None, 3.14],
        # Mixed types (for filter testing)
        "mixed_str_int": ["a", 1, "b", 2, "c", 3],
        "mixed_with_dict": ["x", {"key": "value"}, 1, True],
        # Non-scalar arrays
        "dicts_array": [{"key": "value"}, {"nested": {"item": 123}}],
        # Non-array types
        "object": {"key": "value", "nested": {"item": 123}},
        "string": "hello world",
        "number": 42,
        "boolean": True,
        "null": None,
        # Empty / Single
        "empty_array": [],
        "single_str": ["solo"],
        "single_int": [99],
    }


class TestAsArrayWithOrPattern:
    """Test as_*_array() with `or ()` pattern (recommended)."""

    # --- Basic arrays ---
    def test_as_str_array_or_basic(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["str_array"])
        assert len(value.as_str_array() or ()) == 3

    def test_as_int_array_or_basic(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["int_array"])
        assert len(value.as_int_array() or ()) == 5

    def test_as_float_array_or_basic(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["float_array"])
        assert len(value.as_float_array() or ()) == 3

    def test_as_scalars_or_basic(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["scalars_array"])
        assert len(value.as_scalars() or ()) == 5

    # --- Non-array types ---
    def test_as_str_array_or_non_array(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["object"])
        assert len(value.as_str_array() or ()) == 0

    def test_as_int_array_or_non_array(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["string"])
        assert len(value.as_int_array() or ()) == 0

    def test_as_float_array_or_non_array(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["number"])
        assert len(value.as_float_array() or ()) == 0

    def test_as_scalars_or_non_array(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["boolean"])
        assert len(value.as_scalars() or ()) == 0

    def test_as_scalars_or_null(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["null"])
        assert len(value.as_scalars() or ()) == 0

    # --- Empty array ---
    def test_as_str_array_or_empty(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["empty_array"])
        assert len(value.as_str_array() or ()) == 0

    def test_as_int_array_or_empty(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["empty_array"])
        assert len(value.as_int_array() or ()) == 0

    # --- Mixed type arrays (filtering) ---
    def test_as_str_array_or_mixed(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["mixed_str_int"])
        assert len(value.as_str_array() or ()) == 3  # ["a", "b", "c"]

    def test_as_int_array_or_mixed(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["mixed_str_int"])
        assert len(value.as_int_array() or ()) == 3  # [1, 2, 3]

    def test_as_scalars_or_mixed(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["mixed_with_dict"])
        assert len(value.as_scalars() or ()) == 3  # ["x", 1, True]

    # --- Single element ---
    def test_as_str_array_or_single(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["single_str"])
        assert len(value.as_str_array() or ()) == 1

    def test_as_int_array_or_single(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["single_int"])
        assert len(value.as_int_array() or ()) == 1


class TestAsArrayNoneCheck:
    """Test None check patterns when discrimination is needed."""

    def test_none_check_as_str_array_array(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["str_array"][:2])
        arr = value.as_str_array()
        assert arr is not None
        assert len(arr) == 2  # ["apple", "banana"]

    def test_none_check_as_str_array_none(self, simple_test_data: dict[str, Any]):
        """Verify None when input is a mapping (not an array).

        as_*_array() methods:
        - Return None if the value is not an array type
        - Return empty tuple () if the value is an array but has no matching elements

        This test verifies the first case (non-array input).
        """
        value = Crystallizer.latent(simple_test_data["object"])
        arr = value.as_str_array()
        # Mapping input → arrays=() in SortedJsonValue → None
        assert arr is None

    def test_none_check_as_str_array_empty(self, simple_test_data: dict[str, Any]):
        """Verify empty tuple when input is an empty array.

        This differs from the non-array case: [] is an array, so as_str_array()
        returns () instead of None.

        Comparison:
        - test_none_check_as_str_array_none: Mapping input → None
        - test_none_check_as_str_array_empty: Empty array [] → ()
        """
        value = Crystallizer.latent(simple_test_data["empty_array"])
        arr = value.as_str_array()
        assert arr is not None
        assert len(arr) == 0

    def test_none_check_as_scalars_non_scalar(self, simple_test_data: dict[str, Any]):
        """Verify empty tuple when array has no scalar elements (only dicts)."""
        value = Crystallizer.latent(simple_test_data["dicts_array"])
        arr = value.as_scalars()
        assert arr == ()  # No scalar elements (only dicts)

    def test_none_check_as_int_array_non_int(self, simple_test_data: dict[str, Any]):
        """Verify empty tuple when array has no integer elements (only string)."""
        value = Crystallizer.latent(simple_test_data["mixed_str_int"][:1])
        arr = value.as_int_array()
        assert arr == ()  # No integer elements (only string)


class TestExpectedArray:
    """Test expected_*_array() with type-safe len()."""

    def test_expected_str_array_len(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.strict(simple_test_data["str_array"])
        assert len(value.expected_str_array()) == 3

    def test_expected_int_array_len(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.strict(simple_test_data["int_array"])
        assert len(value.expected_int_array()) == 5

    def test_expected_float_array_len(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.strict(simple_test_data["float_array"])
        assert len(value.expected_float_array()) == 3

    def test_expected_str_array_non_array_raises(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.strict(simple_test_data["object"])
        with pytest.raises(TypeError):
            len(value.expected_str_array())

    def test_expected_int_array_non_array_raises(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.strict(simple_test_data["string"])
        with pytest.raises(TypeError):
            len(value.expected_int_array())

    def test_expected_float_array_non_array_raises(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.strict(simple_test_data["number"])
        with pytest.raises(TypeError):
            len(value.expected_float_array())

    # --- Mode-specific: expected_*() raises TypeError for non-array ---
    def test_expected_str_array_latent_non_array_void(self, simple_test_data: dict[str, Any]):
        """Verify expected_str_array() raises TypeError for non-array in Latent mode."""
        value = Crystallizer.latent(simple_test_data["object"])
        with pytest.raises(TypeError):
            value.expected_str_array()

    def test_expected_str_array_dream_non_array_void(self, simple_test_data: dict[str, Any]):
        """Verify expected_str_array() raises TypeError for non-array in Dream mode."""
        with enable_dream_mode():
            value = Crystallizer.dream(simple_test_data["object"])
            with pytest.raises(TypeError):
                value.expected_str_array()


# type: ignore  # Suppress type checking for TestDirectLenWithoutOr (intentional errors)
class TestDirectLenWithoutOr:
    """Test runtime error: len() fails on tuple[...]|None without `or ()`.

    The return type of as_*_array() is `tuple[...]|None`. Calling len() directly
    will raise TypeError at runtime when the result is None. This test verifies that
    behavior while intentionally suppressing static type checking.

    Recommended pattern:
        len(value.as_str_array() or ())  # Type-safe
        len(value.as_str_array())           # Runtime error if None
    """

    def test_direct_len_as_str_array_runtime_error(self, simple_test_data: dict[str, Any]):
        """Verify runtime error: len() fails on tuple[str, ...]|None."""
        value = Crystallizer.latent(simple_test_data["object"])
        with pytest.raises(TypeError):
            len(value.as_str_array())  # type: ignore

    def test_direct_len_as_int_array_runtime_error(self, simple_test_data: dict[str, Any]):
        """Verify runtime error: len() fails on tuple[int, ...]|None."""
        value = Crystallizer.latent(simple_test_data["string"])
        with pytest.raises(TypeError):
            len(value.as_int_array())  # type: ignore

    def test_direct_len_as_scalars_runtime_error(self, simple_test_data: dict[str, Any]):
        """Verify runtime error: len() fails on tuple[JsonScalar, ...]|None."""
        value = Crystallizer.latent(simple_test_data["number"])
        with pytest.raises(TypeError):
            len(value.as_scalars())  # type: ignore


class TestModeConsistency:
    """Test that `or ()` pattern works consistently across all modes."""

    def test_as_str_array_or_strict(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.strict(simple_test_data["str_array"])
        assert len(value.as_str_array() or ()) == 3

    def test_as_str_array_or_latent(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["str_array"])
        assert len(value.as_str_array() or ()) == 3

    def test_as_str_array_or_dream(self, simple_test_data: dict[str, Any]):
        with enable_dream_mode():
            value = Crystallizer.dream(simple_test_data["str_array"])
            assert len(value.as_str_array() or ()) == 3

    def test_as_str_array_or_non_array_strict(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.strict(simple_test_data["object"])
        # Non-array in strict: to_array() raises, but as_str_array() returns None
        assert len(value.as_str_array() or ()) == 0

    def test_as_str_array_or_non_array_latent(self, simple_test_data: dict[str, Any]):
        value = Crystallizer.latent(simple_test_data["object"])
        assert len(value.as_str_array() or ()) == 0

    def test_as_str_array_or_non_array_dream(self, simple_test_data: dict[str, Any]):
        with enable_dream_mode():
            value = Crystallizer.dream(simple_test_data["object"])
            assert len(value.as_str_array() or ()) == 0
