# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
# SPDX-License-Identifier: Apache-2.0

"""Tests for Pile: Thread-safe typed collection with rich query interface.

Design Philosophy
-----------------
Pile is lionpride's foundational data structure for managing Element collections with:

1. **Thread Safety**: RLock-based synchronization for safe concurrent access
   - All public methods protected by @synchronized/@async_synchronized
   - Recursive locking allows nested pile operations
   - CPython GIL makes dict reads thread-safe (readers get consistent snapshots)
   - O(1) add/remove with zero-copy reads (MappingProxyType views)

2. **Type Validation**: Flexible constraints with Union support
   - strict_type=False: Duck typing, allows subclasses (default)
   - strict_type=True: Exact type match only (no inheritance)
   - Union types: Multi-type collections (Pile[Node | Event])
   - Set-based type algebra for O(1) validation checks

3. **Progression Integration**: Ordered collections with rich query interface

Type Algebra: strict=False allows subclasses, strict=True requires exact match.
Set Operations: include/exclude are idempotent, filters return NEW Piles.

Test Coverage: initialization, basic ops, set ops, queries, async, thread safety,
type validation, serialization, collection protocols, edge cases.
"""

import concurrent.futures
import threading
from uuid import UUID

import pytest
from conftest import TestElement, create_test_elements

from lionpride.core import Element, Pile, Progression
from lionpride.errors import ExistsError, NotFoundError

# =============================================================================
# Test Fixtures
# =============================================================================


# Use lionpride.testing utilities instead of local fixtures
# TestElement imported from testing module


class TypedElement(Element):
    """Typed element for testing type validation."""

    name: str = "test"


class ChildElement(TypedElement):
    """Child element for testing strict_type mode."""

    extra: str = "child"


@pytest.fixture
def simple_items():
    """Create simple test items using testing utilities."""
    return create_test_elements(count=5)


@pytest.fixture
def typed_items():
    """Create typed test items."""
    return [TypedElement(name=f"item{i}") for i in range(3)]


@pytest.fixture
def child_items():
    """Create child test items."""
    return [ChildElement(name=f"child{i}", extra=f"extra{i}") for i in range(2)]


# =============================================================================
# Initialization Tests
# =============================================================================


def test_pile_empty_initialization():
    """Test empty Pile initialization."""
    pile = Pile()
    assert len(pile) == 0
    assert pile.is_empty()
    assert pile.item_type is None
    assert pile.strict_type is False


def test_pile_with_items(simple_items):
    """Test Pile initialization with items."""
    pile = Pile(items=simple_items)
    assert len(pile) == 5
    assert not pile.is_empty()
    for item in simple_items:
        assert item.id in pile


def test_pile_with_item_type(typed_items):
    """Test Pile with item_type validation."""
    pile = Pile(items=typed_items, item_type=TypedElement)
    assert len(pile) == 3
    assert pile.item_type == {TypedElement}


def test_pile_with_strict_type(typed_items):
    """Test Pile with strict_type=True."""
    pile = Pile(items=typed_items, item_type=TypedElement, strict_type=True)
    assert pile.strict_type is True
    # Should allow exact type
    assert len(pile) == 3


def test_pile_with_custom_order(simple_items):
    """Test Pile with custom order."""
    custom_order = [item.id for item in reversed(simple_items)]
    pile = Pile(items=simple_items, order=custom_order)

    # Check order matches custom order
    for i, item in enumerate(pile):
        assert item.id == custom_order[i]


def test_pile_with_progression_order(simple_items):
    """Test Pile with Progression order."""
    custom_order = [item.id for item in reversed(simple_items)]
    prog = Progression(order=custom_order, name="reversed")
    pile = Pile(items=simple_items, order=prog)

    # Check order matches
    for i, item in enumerate(pile):
        assert item.id == custom_order[i]

    # Note: progression name is not preserved in __init__ (only order is used)
    # Name can be manually set after creation if needed
    pile._progression.name = "reversed"
    assert pile._progression.name == "reversed"


def test_pile_order_validation_invalid_uuid(simple_items):
    """Test Pile rejects order with invalid UUID."""
    from uuid import uuid4

    invalid_order = [uuid4()]  # UUID not in items
    with pytest.raises(NotFoundError, match=r"UUID .* not found in items"):
        Pile(items=simple_items, order=invalid_order)


def test_pile_item_type_normalization():
    """Test item_type normalization from various inputs."""
    # Single type
    pile1 = Pile(item_type=TestElement)
    assert pile1.item_type == {TestElement}

    # List of types
    pile2 = Pile(item_type=[TestElement, TypedElement])
    assert pile2.item_type == {TestElement, TypedElement}

    # Set of types
    pile3 = Pile(item_type={TestElement, TypedElement})
    assert pile3.item_type == {TestElement, TypedElement}


# =============================================================================
# Basic Operations Tests
# =============================================================================


def test_add_item():
    """Test adding items to Pile."""
    pile = Pile()
    item = TestElement(value=42)

    pile.add(item)
    assert len(pile) == 1
    assert item.id in pile
    assert pile.get(item.id) == item


def test_add_duplicate_raises_error():
    """Test adding duplicate item raises ExistsError."""
    pile = Pile()
    item = TestElement(value=42)

    pile.add(item)
    with pytest.raises(ExistsError, match="already exists"):
        pile.add(item)


def test_add_with_type_validation():
    """Test add with type validation."""
    pile = Pile(item_type=TypedElement)

    # Valid type
    valid_item = TypedElement(name="valid")
    pile.add(valid_item)
    assert valid_item.id in pile

    # Invalid type
    invalid_item = TestElement(value=42)
    with pytest.raises(TypeError, match="not a subclass"):
        pile.add(invalid_item)


def test_add_with_strict_type():
    """Test add with strict_type=True."""
    pile = Pile(item_type=TypedElement, strict_type=True)

    # Exact type allowed
    exact_item = TypedElement(name="exact")
    pile.add(exact_item)
    assert exact_item.id in pile

    # Subclass not allowed
    child_item = ChildElement(name="child", extra="extra")
    with pytest.raises(TypeError, match="strict_type=True"):
        pile.add(child_item)


def test_remove_item():
    """Test removing items from Pile."""
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    removed = pile.remove(item.id)
    assert len(pile) == 0
    assert removed == item
    assert item.id not in pile


def test_remove_by_element():
    """Test remove accepts Element instance."""
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    removed = pile.remove(item)  # Pass element, not ID
    assert removed == item
    assert len(pile) == 0


def test_remove_nonexistent_raises_error():
    """Test removing nonexistent item raises NotFoundError."""
    from uuid import uuid4

    pile = Pile()
    with pytest.raises(NotFoundError, match="not found"):
        pile.remove(uuid4())


def test_pop_alias():
    """Test pop() is alias for remove()."""
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    popped = pile.pop(item.id)
    assert popped == item
    assert len(pile) == 0


def test_pop_without_default_not_found():
    """Test pop() raises NotFoundError when item not found and no default."""
    from uuid import uuid4

    pile = Pile()

    with pytest.raises(NotFoundError, match="not found in pile"):
        pile.pop(uuid4())


def test_pop_with_default_none():
    """Test pop() returns None when item not found with default=None."""
    from uuid import uuid4

    pile = Pile()
    result = pile.pop(uuid4(), default=None)
    assert result is None


def test_pop_with_custom_default():
    """Test pop() returns custom default when item not found."""
    from uuid import uuid4

    pile = Pile()
    default_item = TestElement(value=999)

    result = pile.pop(uuid4(), default=default_item)
    assert result == default_item


def test_pop_with_default_when_exists():
    """Test pop() returns and removes item when it exists, ignoring default."""
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    default_item = TestElement(value=999)
    popped = pile.pop(item.id, default=default_item)

    assert popped == item
    assert popped != default_item
    assert len(pile) == 0


def test_get_item():
    """Test getting items from Pile."""
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    retrieved = pile.get(item.id)
    assert retrieved == item


def test_get_with_default():
    """Test get with default value."""
    from uuid import uuid4

    pile = Pile()
    default = TestElement(value=999)

    result = pile.get(uuid4(), default=default)
    assert result == default


def test_get_nonexistent_raises_error():
    """Test get without default raises NotFoundError."""
    from uuid import uuid4

    pile = Pile()
    with pytest.raises(NotFoundError, match="not found"):
        pile.get(uuid4())


def test_update_item():
    """Test updating existing item."""
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    # Update with modified item (same ID)
    # Pass id during initialization (frozen field)
    updated_item = TestElement(value=100, id=item.id)
    pile.update(updated_item)

    assert pile.get(item.id).value == 100


def test_update_nonexistent_raises_error():
    """Test updating nonexistent item raises NotFoundError."""
    pile = Pile()
    item = TestElement(value=42)

    with pytest.raises(NotFoundError, match="not found"):
        pile.update(item)


def test_update_with_type_validation():
    """Test update respects type validation."""
    pile = Pile(item_type=TypedElement)
    item = TypedElement(name="original")
    pile.add(item)

    # Invalid type update (pass id during initialization)
    invalid_item = TestElement(value=42, id=item.id)

    with pytest.raises(TypeError, match="not a subclass"):
        pile.update(invalid_item)


def test_clear():
    """Test clearing all items."""
    pile = Pile()
    for i in range(5):
        pile.add(TestElement(value=i))

    assert len(pile) == 5
    pile.clear()
    assert len(pile) == 0
    assert pile.is_empty()


# =============================================================================
# Set-like Operations Tests
# =============================================================================


def test_include_new_item():
    """Test include adds new item and returns True (membership guaranteed)."""
    pile = Pile()
    item = TestElement(value=42)

    result = pile.include(item)
    assert result is True  # Membership guaranteed
    assert item.id in pile


def test_include_existing_item():
    """Test include is idempotent - returns True even if already present."""
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    result = pile.include(item)
    assert result is True  # Still returns True (membership guaranteed)
    assert len(pile) == 1  # Not duplicated


def test_exclude_existing_item():
    """Test exclude removes item and returns True (absence guaranteed)."""
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    result = pile.exclude(item.id)
    assert result is True  # Absence guaranteed
    assert item.id not in pile


def test_exclude_nonexistent_item():
    """Test exclude is idempotent - returns True even if not present."""
    from uuid import uuid4

    pile = Pile()
    result = pile.exclude(uuid4())
    assert result is True  # Returns True (absence guaranteed)


def test_exclude_by_element():
    """Test exclude accepts Element instance."""
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    result = pile.exclude(item)
    assert result is True
    assert len(pile) == 0


def test_include_validation_failure():
    """Test include returns False on validation failure."""
    pile = Pile(item_type=TestElement, strict_type=True)

    # Create incompatible item (different type)
    class OtherElement(Element):
        pass

    item = OtherElement()
    result = pile.include(item)
    assert result is False  # Validation failed
    assert len(pile) == 0  # Item not added


def test_exclude_invalid_id():
    """Test exclude returns False on ID coercion failure."""
    pile = Pile()

    # Invalid ID that cannot be coerced
    result = pile.exclude(12345)  # Not a valid UUID/str/Element
    assert result is False  # ID coercion failed


# =============================================================================
# Query Operations Tests (__getitem__)
# =============================================================================
#
# Rich query interface: Pile's __getitem__ provides type-dispatched access with
# five distinct query modes.
#
# Design Philosophy:
# ------------------
# Python's __getitem__ allows operator overloading for expressive querying:
#     pile[uuid]         → single item (by ID)
#     pile[3]            → single item (by index)
#     pile[1:5]          → Pile[items] (slice)
#     pile[[0, 2]]       → Pile[items] (list of indices)
#     pile[(id1, id2)]   → Pile[items] (tuple of UUIDs)
#     pile[func]         → Pile[items] (predicate)
#     pile[prog]         → Pile[items] (progression)
#
# This unified interface eliminates method proliferation:
#     ❌ pile.get_by_uuid(), pile.get_by_index(), pile.filter_by_func()
#     ✅ pile[key] (single interface, type-dispatched)
#
# Type Dispatch Strategy:
# -----------------------
# 1. UUID/str → get by ID
#    - O(1) dict lookup: self._items[uuid]
#    - Returns single Element
#    - Raises ValueError if not found
#
# 2. int → get by index
#    - O(1) progression index: self._progression[index]
#    - Then O(1) dict lookup: self._items[uuid]
#    - Supports negative indices: pile[-1]
#
# 3. slice → get multiple items
#    - O(k) where k = slice width
#    - Returns new Pile[Element]
#
# 4. list/tuple of int → get multiple items by indices
#    - O(k) where k = list length
#    - Returns new Pile[Element]
#
# 5. list/tuple of UUID → get multiple items by IDs
#    - O(k) where k = list length
#    - Returns new Pile[Element]
#    - Uses progression order: pile[1:3] → [item1, item2]
#
# 4. Progression → filter by progression
#    - O(m) where m = len(progression)
#    - Returns NEW Pile with subset of items
#    - Preserves type constraints (item_type, strict_type)
#
# 5. Callable[[T], bool] → filter by predicate
#    - O(n) full scan with isinstance check
#    - Returns NEW Pile with filtered items
#    - Example: pile[lambda x: x.value > 5]
#
# Immutable Semantics:
# --------------------
# Filter operations (Progression/callable) return NEW Pile:
#     original = Pile(items=[a, b, c])
#     filtered = original[lambda x: x.value > 5]
#     # original unchanged, filtered is new Pile
#
# Why new Pile? Allows chaining without mutation:
#     pile[prog1][lambda x: x.enabled].filter_by_type(Node)
#
# Thread Safety:
# --------------
# All __getitem__ paths use @synchronized:
#     - get_by_uuid: lock held during dict access
#     - get_by_index: lock held during progression + dict access
#     - filter ops: lock held during iteration (snapshot created)
#     - Returned Piles are independent (no shared state)
#
# Performance Notes:
# ------------------
# - Index access: O(1) via Progression's __getitem__ optimization
# - Slice: O(k) where k = slice width (not O(n) iteration)
# - Progression filter: O(m) where m = progression length (subset iteration)
# - Callable filter: O(n) full scan (cannot optimize without indexing)
#
# Overload Annotations:
# ---------------------
# Type checker sees different return types:
#     @overload
#     def __getitem__(self, key: UUID) -> T: ...
#     def __getitem__(self, key: int) -> T: ...
#     def __getitem__(self, key: slice) -> list[T]: ...
#     def __getitem__(self, key: Progression) -> Pile[T]: ...
#     def __getitem__(self, key: Callable[[T], bool]) -> Pile[T]: ...


def test_getitem_by_uuid(simple_items):
    """Test __getitem__ with UUID."""
    pile = Pile(items=simple_items)
    item = simple_items[0]

    retrieved = pile[item.id]
    assert retrieved == item


def test_getitem_by_str(simple_items):
    """Test __getitem__ with string ID."""
    pile = Pile(items=simple_items)
    item = simple_items[0]

    retrieved = pile[str(item.id)]
    assert retrieved == item


def test_getitem_by_index(simple_items):
    """Test __getitem__ with int index."""
    pile = Pile(items=simple_items)

    # Positive index
    assert pile[0] == simple_items[0]
    assert pile[2] == simple_items[2]

    # Negative index
    assert pile[-1] == simple_items[-1]


def test_getitem_by_slice(simple_items):
    """Test __getitem__ with slice."""
    pile = Pile(items=simple_items)

    # Slice returns Pile (changed from list in alpha7)
    result = pile[1:3]
    assert isinstance(result, Pile)
    assert len(result) == 2
    assert list(result) == simple_items[1:3]


def test_getitem_by_list_of_indices(simple_items):
    """Test __getitem__ with list of int indices."""
    pile = Pile(items=simple_items)

    # List of indices returns Pile
    result = pile[[0, 2, 3]]
    assert isinstance(result, Pile)
    assert len(result) == 3
    assert list(result) == [simple_items[0], simple_items[2], simple_items[3]]

    # Order preserved
    result_reversed = pile[[3, 0, 1]]
    assert list(result_reversed) == [simple_items[3], simple_items[0], simple_items[1]]


def test_getitem_by_tuple_of_indices(simple_items):
    """Test __getitem__ with tuple of int indices."""
    pile = Pile(items=simple_items)

    # Tuple of indices returns Pile
    result = pile[(1, 2)]
    assert isinstance(result, Pile)
    assert len(result) == 2
    assert list(result) == [simple_items[1], simple_items[2]]


def test_getitem_by_list_of_uuids(simple_items):
    """Test __getitem__ with list of UUIDs."""
    pile = Pile(items=simple_items)

    # List of UUIDs returns Pile
    uuids = [simple_items[1].id, simple_items[3].id]
    result = pile[uuids]
    assert isinstance(result, Pile)
    assert len(result) == 2
    assert simple_items[1].id in result
    assert simple_items[3].id in result

    # Order preserved
    result_list = list(result)
    assert result_list[0].id == simple_items[1].id
    assert result_list[1].id == simple_items[3].id


def test_getitem_by_tuple_of_uuids(simple_items):
    """Test __getitem__ with tuple of UUIDs."""
    pile = Pile(items=simple_items)

    # Tuple of UUIDs returns Pile
    uuids = (simple_items[0].id, simple_items[2].id)
    result = pile[uuids]
    assert isinstance(result, Pile)
    assert len(result) == 2
    assert simple_items[0].id in result
    assert simple_items[2].id in result


def test_getitem_list_empty_raises(simple_items):
    """Test __getitem__ with empty list/tuple raises ValueError."""
    pile = Pile(items=simple_items)

    with pytest.raises(ValueError, match="empty list/tuple"):
        _ = pile[[]]

    with pytest.raises(ValueError, match="empty list/tuple"):
        _ = pile[()]


def test_getitem_list_mixed_types_raises(simple_items):
    """Test __getitem__ with mixed int/UUID raises TypeError."""
    pile = Pile(items=simple_items)

    # Int first, UUID later (line 501)
    with pytest.raises(TypeError, match="Cannot mix int and UUID"):
        _ = pile[[0, simple_items[1].id]]

    # UUID first, int later (line 507)
    with pytest.raises(TypeError, match="Cannot mix int and UUID"):
        _ = pile[[simple_items[0].id, 1]]


def test_getitem_list_invalid_type_raises(simple_items):
    """Test __getitem__ with invalid type in list raises TypeError."""
    pile = Pile(items=simple_items)

    # Float is neither int nor UUID (line 513)
    with pytest.raises(TypeError, match="list/tuple must contain only int or UUID"):
        _ = pile[[1.5, 2.5]]


def test_getitem_by_progression(simple_items):
    """Test __getitem__ with Progression."""
    pile = Pile(items=simple_items)

    # Create progression with subset of items
    subset_ids = [simple_items[1].id, simple_items[3].id]
    prog = Progression(order=subset_ids)

    filtered_pile = pile[prog]
    assert isinstance(filtered_pile, Pile)
    assert len(filtered_pile) == 2
    assert simple_items[1].id in filtered_pile
    assert simple_items[3].id in filtered_pile


def test_getitem_by_callable(simple_items):
    """Test __getitem__ with callable predicate."""
    pile = Pile(items=simple_items)

    # Filter by value
    filtered_pile = pile[lambda x: x.value >= 3]
    assert isinstance(filtered_pile, Pile)
    assert len(filtered_pile) == 2
    assert all(item.value >= 3 for item in filtered_pile)


def test_getitem_invalid_key_type(simple_items):
    """Test __getitem__ with invalid key type raises TypeError."""
    pile = Pile(items=simple_items)

    with pytest.raises(TypeError, match="Invalid key type"):
        _ = pile[3.14]  # float not supported


def test_filter_by_progression_returns_new_pile(simple_items):
    """Test _filter_by_progression returns NEW Pile."""
    pile = Pile(items=simple_items)
    subset_ids = [simple_items[0].id, simple_items[2].id]
    prog = Progression(order=subset_ids)

    filtered_pile = pile[prog]

    # Verify new pile created
    assert filtered_pile is not pile
    assert filtered_pile.item_type == pile.item_type
    assert filtered_pile.strict_type == pile.strict_type


def test_filter_by_function_returns_new_pile(simple_items):
    """Test _filter_by_function returns NEW Pile."""
    pile = Pile(items=simple_items)

    filtered_pile = pile[lambda x: x.value < 3]

    # Verify new pile created
    assert filtered_pile is not pile
    assert filtered_pile.item_type == pile.item_type


def test_filter_by_type(simple_items, typed_items):
    """Test filter_by_type method."""
    # Create pile with mixed types
    pile = Pile(items=simple_items + typed_items, item_type={TestElement, TypedElement})

    # Filter by TestElement
    simple_pile = pile.filter_by_type(TestElement)
    assert len(simple_pile) == 5
    assert all(isinstance(item, TestElement) for item in simple_pile)


def test_filter_by_type_with_strict_validation():
    """Test filter_by_type validates against pile's item_type."""
    pile = Pile(item_type=TypedElement)

    # Try to filter by incompatible type
    with pytest.raises(TypeError, match="not compatible"):
        pile.filter_by_type(TestElement)


def test_filter_by_type_no_matches():
    """Test filter_by_type raises NotFoundError when no matches."""
    pile = Pile(items=[TestElement(value=1)])

    with pytest.raises(NotFoundError, match="No items of type"):
        pile.filter_by_type(TypedElement)


def test_filter_by_type_strict_mode_invalid_types():
    """Test filter_by_type in strict mode rejects invalid types."""
    pile = Pile(items=[TypedElement(name="test")], item_type=TypedElement, strict_type=True)

    # Try to filter by type not in allowed set
    with pytest.raises(TypeError, match="not allowed in pile"):
        pile.filter_by_type(TestElement)


# =============================================================================
# Async Operations Tests
# =============================================================================


@pytest.mark.asyncio
async def test_async_add():
    """Test async add operation using context manager."""
    pile = Pile()
    item = TestElement(value=42)

    async with pile:
        pile.add(item)

    assert len(pile) == 1
    assert item.id in pile


@pytest.mark.asyncio
async def test_async_add_duplicate_raises_error():
    """Test async add of duplicate raises ExistsError."""
    pile = Pile()
    item = TestElement(value=42)

    async with pile:
        pile.add(item)
        with pytest.raises(ExistsError, match="already exists"):
            pile.add(item)


@pytest.mark.asyncio
async def test_async_remove():
    """Test async remove operation using context manager."""
    pile = Pile()
    item = TestElement(value=42)

    async with pile:
        pile.add(item)
        removed = pile.remove(item.id)

    assert removed == item
    assert len(pile) == 0


@pytest.mark.asyncio
async def test_async_remove_nonexistent_raises_error():
    """Test async remove of nonexistent item raises NotFoundError."""
    from uuid import uuid4

    pile = Pile()
    async with pile:
        with pytest.raises(NotFoundError, match="not found"):
            pile.remove(uuid4())


@pytest.mark.asyncio
async def test_async_get():
    """Test async get operation using context manager."""
    pile = Pile()
    item = TestElement(value=42)

    async with pile:
        pile.add(item)
        retrieved = pile.get(item.id)

    assert retrieved == item


@pytest.mark.asyncio
async def test_async_get_nonexistent_raises_error():
    """Test async get of nonexistent item raises NotFoundError."""
    from uuid import uuid4

    pile = Pile()
    async with pile:
        with pytest.raises(NotFoundError, match="not found"):
            pile.get(uuid4())


@pytest.mark.asyncio
async def test_async_context_manager():
    """Test async context manager (__aenter__ / __aexit__)."""
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    # Context manager acquires lock, so we access internal state directly
    async with pile as p:
        assert p is pile
        # Direct access to internal state (lock already held)
        assert len(p._items) == 1
        assert item.id in p._items
        assert p._items[item.id] == item


@pytest.mark.asyncio
async def test_concurrent_async_operations():
    """Test concurrent async operations using context manager."""
    pile = Pile()
    items = [TestElement(value=i) for i in range(10)]

    # Add items in async context
    async with pile:
        for item in items:
            pile.add(item)

    assert len(pile) == 10

    # Get items in async context
    async with pile:
        results = [pile.get(item.id) for item in items]

    assert len(results) == 10
    assert all(r in items for r in results)


# =============================================================================
# Pile Error Handling Tests (Traceback Suppression)
# =============================================================================


def test_remove_suppresses_keyerror():
    """Verify Pile.remove uses 'from None' to suppress KeyError traceback."""
    import traceback
    from uuid import uuid4

    pile = Pile()
    fake_id = uuid4()

    try:
        pile.remove(fake_id)
    except NotFoundError as e:
        tb = traceback.format_exception(type(e), e, e.__traceback__)
        tb_str = "".join(tb)

        # Should NOT contain KeyError or "During handling" context
        assert "KeyError" not in tb_str, "KeyError should be suppressed by 'from None'"
        assert "During handling" not in tb_str, "Exception context should be suppressed"


def test_get_suppresses_keyerror():
    """Verify Pile.get uses 'from None' to suppress KeyError traceback."""
    import traceback
    from uuid import uuid4

    pile = Pile()
    fake_id = uuid4()

    try:
        pile.get(fake_id)
    except NotFoundError as e:
        tb = traceback.format_exception(type(e), e, e.__traceback__)
        tb_str = "".join(tb)

        assert "KeyError" not in tb_str, "KeyError should be suppressed by 'from None'"
        assert "During handling" not in tb_str, "Exception context should be suppressed"


# =============================================================================
# Thread Safety Tests
# =============================================================================
#
# Thread-safety mechanisms: Pile achieves safe concurrent access through RLock-based
# synchronization with zero-copy reads.
#
# Synchronization Strategy:
# --------------------------
# 1. **RLock (Reentrant Lock)**: threading.RLock
#    - Allows same thread to acquire lock multiple times
#    - Required for nested operations: pile.add → pile._validate_type
#    - Prevents deadlocks in recursive validation
#
# 2. **@synchronized decorator**: Function-level protection
#    - Wraps with self._lock.acquire() / release()
#    - Applied to all mutating operations: add, remove, update, clear
#    - Read operations also synchronized for consistency
#
# 3. **AsyncLock**: For async operations
#    - Separate lock for __aenter__ / __aexit__
#    - async with pile: acquires _async_lock
#    - Independent of sync lock (no cross-contamination)
#
# 4. **Immutable views**: Zero-copy reads
#    - pile.items returns MappingProxyType (read-only dict wrapper)
#    - pile.progression returns copy (Progression(order=list(...)))
#    - No lock needed for reads (CPython GIL makes dict reads thread-safe)
#
# Concurrency Invariants:
# ------------------------
#     □(op ∈ {add, remove, get, update} ⇒ atomic)    [RLock serializes ops]
#     □(∀t: pile[uuid] consistent)                    [no partial updates]
#     □(∀t: len(pile) = |_items| = |_progression|)   [structural integrity]
#     □(readers never blocked by readers)             [dict is GIL-protected]
#
# Race Condition Scenarios:
# -------------------------
# 1. Duplicate add (same UUID):
#    Thread1: pile.add(x) → checks "x.id in _items" under lock → adds
#    Thread2: pile.add(x) → waits for lock → checks → raises ValueError
#    Result: Exactly one thread succeeds (atomicity guaranteed)
#
# 2. Concurrent remove + get:
#    Thread1: pile.remove(uuid) → acquires lock → removes from dict + progression
#    Thread2: pile.get(uuid) → waits for lock → ValueError (item not found)
#    Result: Serialized access (no torn reads)
#
# 3. Filter operations (immutable semantics):
#    Thread1: filtered1 = pile[lambda x: x.value > 5] → creates NEW Pile
#    Thread2: filtered2 = pile[lambda x: x.value < 3] → creates NEW Pile
#    Result: Both succeed, original pile unchanged (no locking contention)
#
# Performance Characteristics:
# ----------------------------
# - Lock contention: O(1) dict ops minimize hold time
# - No lock convoy: Readers don't block readers (GIL handles dict reads)
# - Filter ops: No contention (creates new Pile, source unchanged)
# - Progression ops: O(n) but single-threaded (progression.remove is linear scan)
#
# Test Strategy:
# --------------
# ThreadPoolExecutor with 4 workers stress-tests:
# - 20+ concurrent adds (unique items)
# - 10+ concurrent removes (partial removal)
# - 10+ concurrent gets (read-heavy workload)
# - Mixed operations (add/remove/get interleaved)


def test_thread_safe_add():
    """Test thread-safe add operations."""
    pile = Pile()
    items = [TestElement(value=i) for i in range(20)]

    def add_item(item):
        pile.add(item)

    # Add items from multiple threads
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
        executor.map(add_item, items)

    assert len(pile) == 20
    for item in items:
        assert item.id in pile


def test_thread_safe_remove():
    """Test thread-safe remove operations."""
    pile = Pile()
    items = [TestElement(value=i) for i in range(20)]
    for item in items:
        pile.add(item)

    def remove_item(item):
        pile.remove(item.id)

    # Remove items from multiple threads
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
        executor.map(remove_item, items[:10])

    assert len(pile) == 10


def test_thread_safe_get():
    """Test thread-safe get operations."""
    pile = Pile()
    items = [TestElement(value=i) for i in range(10)]
    for item in items:
        pile.add(item)

    results = []
    lock = threading.Lock()

    def get_item(item):
        retrieved = pile.get(item.id)
        with lock:
            results.append(retrieved)

    # Get items from multiple threads
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
        executor.map(get_item, items)

    assert len(results) == 10
    assert all(r in items for r in results)


def test_thread_safe_mixed_operations():
    """Test thread-safe mixed operations (add, remove, get)."""
    pile = Pile()
    initial_items = [TestElement(value=i) for i in range(10)]
    for item in initial_items:
        pile.add(item)

    results = []
    lock = threading.Lock()

    def add_items():
        for i in range(10, 15):
            pile.include(TestElement(value=i))

    def remove_items():
        for item in initial_items[:5]:
            pile.exclude(item.id)

    def get_items():
        for item in initial_items[5:]:
            try:
                result = pile.get(item.id)
                with lock:
                    results.append(result)
            except ValueError:
                pass

    # Run operations from multiple threads
    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        futures = [
            executor.submit(add_items),
            executor.submit(remove_items),
            executor.submit(get_items),
        ]
        concurrent.futures.wait(futures)

    # Verify final state
    assert 10 <= len(pile) <= 15


# =============================================================================
# Type Validation Tests
# =============================================================================
#
# Type validation strategy: Pile supports flexible type constraints while maintaining
# runtime safety through set-based type algebra.
#
# Design Rationale:
# -----------------
# 1. **Set-based validation**: O(1) type checking via set membership
#    - item_type stored as set[type] for efficient lookup
#    - Normalized from Union/list/single type during initialization
#
# 2. **Dual modes**:
#    - strict_type=False (default): Duck typing, allows subclasses
#      ∀x: valid(x) ⟺ ∃t ∈ I: isinstance(x, t)
#    - strict_type=True: Exact type match, no inheritance
#      ∀x: valid(x) ⟺ type(x) ∈ I
#
# 3. **Union type support**: Pile[Node | Event]
#    - Union[A, B] normalized to {A, B}
#    - Multi-type collections with single validation pass
#
# 4. **Fail-fast validation**:
#    - Type checked at add() time (not lazy)
#    - Deserialization validates ALL items before constructing Pile
#    - Invalid types raise TypeError immediately
#
# Thread Safety:
# --------------
# _validate_type() called within @synchronized methods (no lock needed)
# Set membership checks are atomic (immutable set, no races)
#
# Performance:
# ------------
# Permissive mode: O(k) where k = |item_type| (isinstance checks)
# Strict mode: O(1) (set membership: type(x) in item_type)


def test_validate_type_not_element():
    """Test _validate_type rejects non-Element."""
    pile = Pile()

    with pytest.raises(TypeError, match="must be Element subclass"):
        pile._validate_type("not an element")


def test_validate_type_permissive_allows_subclass():
    """Test permissive mode allows subclasses."""
    pile = Pile(item_type=TypedElement, strict_type=False)
    child_item = ChildElement(name="child", extra="extra")

    # Should not raise
    pile._validate_type(child_item)


def test_validate_type_strict_rejects_subclass():
    """Test strict mode rejects subclasses."""
    pile = Pile(item_type=TypedElement, strict_type=True)
    child_item = ChildElement(name="child", extra="extra")

    with pytest.raises(TypeError, match="strict_type=True"):
        pile._validate_type(child_item)


def test_validate_type_strict_allows_exact_type():
    """Test strict mode allows exact type."""
    pile = Pile(item_type=TypedElement, strict_type=True)
    exact_item = TypedElement(name="exact")

    # Should not raise
    pile._validate_type(exact_item)


def test_validate_type_multiple_allowed_types():
    """Test validation with multiple allowed types."""
    pile = Pile(item_type={TestElement, TypedElement})

    # Both types should be allowed
    simple_item = TestElement(value=42)
    typed_item = TypedElement(name="test")

    pile._validate_type(simple_item)
    pile._validate_type(typed_item)


# =============================================================================
# Type Constraints Edge Cases Tests
# =============================================================================


class TestPileTypeConstraintsEdgeCases:
    """
    Test Pile type constraint edge cases and adapter registry isolation.

    Edge Cases:
        - Permissive mode with unrelated types: Rejects non-subclass types
        - Adapter registry isolation: Subclass registries independent

    Scenarios:
        - Deserialize with unrelated type in permissive mode
        - Verify isolated adapter registries per subclass

    Invariants Tested:
        - Permissive mode: Allows subclasses, rejects unrelated types
        - Adapter registry: Rust-like explicit pattern (no pollution)

    Design Rationale:
        Permissive vs Strict:
            - Permissive: Duck typing for subclasses (flexible)
            - Still validates type hierarchy (safety)
            - Rejects unrelated types (not "anything goes")

        Adapter Isolation:
            - Each subclass has independent registry
            - Prevents adapter pollution across class hierarchy
            - Explicit registration required (Rust-like safety)
    """

    def test_from_dict_permissive_mode_incompatible_type(self):
        """
        Test permissive mode rejects unrelated types (not subclasses).

        Pattern:
            Permissive type validation with inheritance hierarchy

        Edge Case:
            Type constraint allows subclasses, but not unrelated types

        Scenario:
            1. Create pile with ChildElement (subclass of TypedElement)
            2. Serialize to dict
            3. Change item_type to unrelated type (TestElement)
            4. Attempt deserialization in permissive mode

        Expected:
            TypeError: ChildElement is not a subclass of TestElement

        Design Trade-off:
            Permissive mode allows subclasses for flexibility, but still
            validates type hierarchy for safety. Not "anything goes".

        Use Case:
            Deserializing heterogeneous collections from external sources
            where type metadata may be modified or incorrect.

        Complexity:
            Type check: O(1) set membership + O(len(mro)) isinstance
        """
        # Create pile with TypeC (child of TypeB)
        child_item = ChildElement(name="child", extra="extra")
        pile = Pile(items=[child_item])
        data = pile.to_dict()

        # Change item_type to unrelated type (TestElement)
        data["item_type"] = [f"{TestElement.__module__}.{TestElement.__name__}"]
        data["strict_type"] = False

        # Should fail because ChildElement is not a subclass of TestElement
        # (permissive mode allows subclasses, but not unrelated types)
        with pytest.raises(TypeError, match="is not a subclass of any allowed type"):
            Pile.from_dict(data)

    def test_isolated_registry_per_subclass(self):
        """
        Test that each Pile subclass has its own isolated adapter registry.

        Pattern:
            Rust-like explicit adapter pattern (no implicit inheritance)

        Edge Case:
            Adapter registration is per-class, not shared across hierarchy

        Scenario:
            1. Create two Pile subclasses (PileA, PileB)
            2. Verify neither has adapters initially
            3. Attempt to use unregistered adapter (toml)
            4. Verify AdapterNotFoundError raised for both

        Expected:
            Both subclasses have independent registries (no shared state)

        Design Rationale:
            Why explicit adapters?
                - Prevents adapter pollution across hierarchy
                - Explicit registration required (Rust-like safety)
                - Each subclass controls its own adapter set

        Architecture:
            pydapter creates unique registry attribute per class using id(cls).
            This ensures complete isolation between subclasses.

        Use Case:
            Domain-specific Pile subclasses with different serialization needs
            (e.g., FilePile with file:// adapters, DBPile with db:// adapters).

        Complexity:
            Registry lookup: O(1) via class ID-based attribute name
        """

        # Create two Pile subclasses
        class PileA(Pile):
            pass

        class PileB(Pile):
            pass

        # Both should have independent registries (no adapters initially)
        from pydapter.exceptions import AdapterNotFoundError

        pile_a = PileA([Element()])
        pile_b = PileB([Element()])

        # Neither has toml adapter (isolated registries)
        try:
            pile_a.adapt_to("toml")
            raise AssertionError("PileA should not have toml adapter")
        except AdapterNotFoundError:
            pass  # Expected

        try:
            pile_b.adapt_to("toml")
            raise AssertionError("PileB should not have toml adapter")
        except AdapterNotFoundError:
            pass  # Expected


# =============================================================================
# Serialization Tests
# =============================================================================
#
# Serialization strategy: Pile preserves progression order and type constraints
# through to_dict/from_dict round-trips with three output modes.
#
# Design Philosophy:
# ------------------
# Pile extends Element serialization with:
# 1. **Progression order preservation**: Items serialized in order
# 2. **Type metadata**: item_type set serialized as list[str]
# 3. **Validation on deserialization**: Fail-fast type checking
# 4. **Mode-aware**: python/json/db with different formats
#
# Output Modes:
# -------------
# 1. python (default):
#    - UUIDs as UUID objects
#    - datetime as datetime objects
#    - Items as dict with Python types
#    - For in-memory transfer (no JSON serialization needed)
#
# 2. json:
#    - UUIDs as str
#    - datetime as ISO format str
#    - JSON-safe primitives only
#    - For API responses, file storage
#
# 3. db:
#    - Like json, but metadata → node_metadata (db column naming)
#    - For PostgreSQL/Qdrant storage via pydapter
#
# Progression Preservation:
# --------------------------
# Progression order is IMPLICIT in items array:
#     data["items"] = [items[uuid] for uuid in progression]
#     # Order recovered during from_dict by add() sequence
#
# Progression name stored in metadata:
#     if progression.name:
#         data["metadata"]["progression_name"] = progression.name
#
# Why implicit order? Token efficiency:
#     ❌ {items: [...], order: [uuid1, uuid2, ...]} (duplicates UUIDs)
#     ✅ {items: [dict1, dict2, ...]} (order = iteration order)
#
# Type Serialization:
# -------------------
# item_type: set[type] → list[str]:
#     {TestElement, TypedElement}
#     → ["tests.base.test_pile.TestElement", "tests.base.test_pile.TypedElement"]
#
# Deserialization: list[str] → set[type]:
#     load_type_from_string() imports each type
#     Pydantic validator normalizes to set[type]
#
# Fail-Fast Validation:
# ---------------------
# from_dict() validates ALL items BEFORE adding:
#     for item_dict in items_data:
#         lion_class = item_dict["metadata"]["lion_class"]
#         validate_type(load_type(lion_class))  # Check before Element.from_dict
#     # Only if all valid, construct Pile + add items
#
# Why fail-fast? Prevents partial deserialization:
#     ❌ Add 10 items, fail on 11th → Pile has 10 items (inconsistent state)
#     ✅ Validate 11 items, fail on 11th → Raise TypeError, no Pile created
#
# Thread Safety:
# --------------
# to_dict(): @synchronized ensures consistent snapshot
#     - Items serialized in progression order
#     - No concurrent modifications during serialization
#
# from_dict(): No locking needed (class method, creates new Pile)
#     - Adds items sequentially (single-threaded initialization)
#
# Round-Trip Guarantees:
# ----------------------
#     pile1 = Pile(items=[...], item_type=T, strict_type=True)
#     data = pile1.to_dict(mode="json")
#     pile2 = Pile.from_dict(data)
#
# Invariants preserved:
#     □(pile1.item_type == pile2.item_type)
#     □(pile1.strict_type == pile2.strict_type)
#     □(list(pile1) == list(pile2))  [order preserved]
#     □(pile1._progression.name == pile2._progression.name)  [metadata preserved]
#     □(∀item: item.id in pile1 ⟺ item.id in pile2)  [membership preserved]
#
# Performance:
# ------------
# to_dict(): O(n) where n = len(pile)
#     - Iterate progression: O(n)
#     - Serialize each item: O(n) x O(item serialization)
#
# from_dict(): O(n) where n = len(items)
#     - Validate all types: O(n) x O(1) [set membership]
#     - Deserialize items: O(n) x O(Element.from_dict)
#     - Add items: O(n) x O(1) [dict insert + progression append]


def test_to_dict_python_mode(simple_items):
    """Test to_dict in python mode."""
    pile = Pile(items=simple_items)
    data = pile.to_dict(mode="python")

    assert "items" in data
    assert len(data["items"]) == 5
    assert isinstance(data["id"], UUID)  # UUID object in python mode


def test_to_dict_json_mode(simple_items):
    """Test to_dict in json mode."""
    pile = Pile(items=simple_items)
    data = pile.to_dict(mode="json")

    assert "items" in data
    assert len(data["items"]) == 5
    assert isinstance(data["id"], str)  # String in json mode


def test_to_dict_preserves_progression_order(simple_items):
    """Test to_dict preserves progression order."""
    reversed_order = [item.id for item in reversed(simple_items)]
    pile = Pile(items=simple_items, order=reversed_order)
    data = pile.to_dict()

    # Items should be in reversed order
    for i, item_dict in enumerate(data["items"]):
        assert item_dict["id"] == reversed_order[i]


def test_to_dict_preserves_progression_name(simple_items):
    """Test to_dict preserves progression name in metadata."""
    pile = Pile(items=simple_items)
    # Manually set progression name (not preserved from __init__)
    pile._progression.name = "custom_order"
    data = pile.to_dict()

    # progression_name only added if progression has a name
    assert "metadata" in data
    assert "progression_name" in data["metadata"]
    assert data["metadata"]["progression_name"] == "custom_order"


def test_to_dict_adds_metadata_for_progression_name():
    """Test to_dict creates metadata dict if needed for progression name."""
    # Create pile with no metadata
    pile = Pile()
    pile.add(TestElement(value=1))
    # Clear metadata and set progression name
    pile.metadata = {}
    pile._progression.name = "test_prog"

    pile.to_dict(exclude={"metadata"})  # Exclude existing metadata
    # Re-serialize with metadata
    data2 = pile.to_dict()

    assert "metadata" in data2
    assert "progression_name" in data2["metadata"]


def test_serialize_item_type():
    """Test item_type serialization."""
    pile = Pile(item_type={TestElement, TypedElement})
    data = pile.to_dict()

    assert "item_type" in data
    assert isinstance(data["item_type"], list)
    assert len(data["item_type"]) == 2
    # Should be module paths
    assert all("." in type_str for type_str in data["item_type"])


def test_from_dict_basic(simple_items):
    """Test from_dict basic deserialization."""
    pile = Pile(items=simple_items)
    data = pile.to_dict()

    restored = Pile.from_dict(data)
    assert len(restored) == len(pile)
    for item in simple_items:
        assert item.id in restored


def test_from_dict_preserves_item_type():
    """Test from_dict preserves item_type."""
    pile = Pile(item_type=TestElement)
    data = pile.to_dict()

    restored = Pile.from_dict(data)
    assert restored.item_type == {TestElement}


def test_from_dict_preserves_strict_type():
    """Test from_dict preserves strict_type."""
    pile = Pile(item_type=TypedElement, strict_type=True)
    data = pile.to_dict()

    restored = Pile.from_dict(data)
    assert restored.strict_type is True


def test_from_dict_preserves_progression_name(simple_items):
    """Test from_dict preserves progression name."""
    pile = Pile(items=simple_items)
    # Manually set progression name
    pile._progression.name = "test_order"
    data = pile.to_dict()

    restored = Pile.from_dict(data)
    # Check internal progression (progression property returns copy)
    assert restored._progression.name == "test_order"


def test_from_dict_validates_types_strict_mode():
    """Test from_dict validates types in strict mode."""
    # Create pile with mixed types
    child_item = ChildElement(name="child", extra="extra")
    pile = Pile(items=[child_item])
    data = pile.to_dict()

    # Try to deserialize with strict parent type
    data["item_type"] = [f"{TypedElement.__module__}.{TypedElement.__name__}"]
    data["strict_type"] = True

    # Should fail because ChildElement is not exact match for TypedElement
    with pytest.raises(TypeError, match="strict_type=True"):
        Pile.from_dict(data)


def test_from_dict_validates_types_permissive_mode():
    """Test from_dict validates types in permissive mode."""
    # Create pile with child type
    child_item = ChildElement(name="child", extra="extra")
    pile = Pile(items=[child_item])
    data = pile.to_dict()

    # Deserialize with parent type (permissive)
    data["item_type"] = [f"{TypedElement.__module__}.{TypedElement.__name__}"]
    data["strict_type"] = False

    # Should succeed because ChildElement is subclass of TypedElement
    restored = Pile.from_dict(data)
    assert len(restored) == 1


# =============================================================================
# Serialization Edge Cases Tests
# =============================================================================


class TestPileSerializationEdgeCases:
    """
    Test Pile serialization edge cases and error handling.

    Edge Cases:
        - Union type serialization: Union[A, B] → {A, B}
        - Set type serialization: set → list → set round-trip
        - Runtime type objects: Type objects vs strings
        - Invalid lion_class: Validation error handling
        - Metadata modes: db mode with custom meta_key

    Scenarios:
        - Serialize/deserialize Union types
        - Serialize/deserialize set-based type constraints
        - Programmatic from_dict with type objects
        - Validation with invalid/missing type metadata
        - Mode-specific metadata handling

    Invariants Tested:
        - Type normalization: Union/set → set internally
        - Serialization format: set → list (JSON-safe)
        - Deserialization: list/Union/type objects → set
        - Validation: Graceful error handling for invalid metadata
        - Metadata preservation: Across all modes

    Design Rationale:
        Union Type Support:
            - Union[A, B] normalized to {A, B} for O(1) validation
            - Set-based algebra for efficient type checking
            - Serialization uses list (JSON-compatible)

        Validation Strategy:
            - Fail-fast: Validate before deserialization
            - Graceful degradation: Skip invalid metadata, continue validation
            - Error late: Element.from_dict handles actual deserialization errors

        Metadata Handling:
            - Mode-aware: python/json/db have different formats
            - Custom meta_key: db mode supports column name customization
            - Progression name: Stored in metadata for restoration
    """

    def test_from_dict_with_union_item_type(self):
        """
        Test Union type serialization and deserialization.

        Pattern:
            Union type normalization through full serialization cycle

        Edge Case:
            Union[TypeA, TypeB] → {TypeA, TypeB} → list → {TypeA, TypeB}

        Scenario:
            1. Create pile with Union[TypeA, TypeB] type constraint
            2. Add items of both types
            3. Serialize to dict
            4. Deserialize from dict
            5. Verify type constraint restored as set

        Expected:
            - Union normalized to set: {TypeA, TypeB}
            - Serialization: set → list of strings
            - Deserialization: list → set of types
            - Type constraint preserved through round-trip

        Design Rationale:
            Why normalize Union to set?
                - O(1) type validation via set membership
                - Consistent internal representation
                - Set-based type algebra for multi-type constraints

        Use Case:
            Heterogeneous collections with explicit type constraints:
            Pile[Node | Event] for workflow systems.

        Complexity:
            - Union extraction: O(k) where k = number of union members
            - Type validation: O(1) set membership check
        """
        from typing import Union

        # Create pile with Union type
        pile = Pile(item_type=Union[TestElement, TypedElement])
        pile.add(TestElement(value=1))
        pile.add(TypedElement(name="test"))

        # Serialize
        data = pile.to_dict()

        # Deserialize - extract_types handles Union → set conversion
        restored = Pile.from_dict(data)

        assert len(restored) == 2
        assert restored.item_type == {TestElement, TypedElement}

    def test_from_dict_with_set_item_type(self):
        """
        Test set-based type constraint serialization round-trip.

        Pattern:
            Set → list → set serialization for JSON compatibility

        Edge Case:
            Python set → JSON list (JSON has no set type)

        Scenario:
            1. Create pile with set of types: {TypeA, TypeB}
            2. Serialize to dict
            3. Verify item_type serialized as list
            4. Deserialize from dict
            5. Verify item_type restored as set

        Expected:
            - item_type: set → list → set
            - JSON-safe format (list, not set)
            - Set semantics preserved (no duplicate types)

        Design Rationale:
            Why serialize to list?
                - JSON has no set type (compatibility)
                - List preserves all type references
                - Deserialization normalizes back to set

        Use Case:
            API responses, file storage where JSON format required
            but set semantics desired internally.

        Complexity:
            - Serialization: O(k) where k = |item_type|
            - Deserialization: O(k) list → set conversion
        """
        # Create pile with set of types
        pile = Pile(item_type={TestElement, TypedElement})
        pile.add(TestElement(value=1))

        data = pile.to_dict()

        # item_type serializes to list
        assert isinstance(data["item_type"], list)

        # Deserialize - should handle list correctly
        restored = Pile.from_dict(data)
        assert restored.item_type == {TestElement, TypedElement}

    def test_from_dict_strict_mode_exact_match_in_list(self):
        """
        Test strict mode allows exact type match in deserialization.

        Pattern:
            Strict type validation positive case (valid exact match)

        Edge Case:
            Strict mode with valid exact type (not just rejection tests)

        Scenario:
            1. Create pile with exact type in strict mode
            2. Serialize to dict
            3. Deserialize from dict
            4. Verify deserialization succeeds

        Expected:
            - Strict mode: type(item) ∈ item_type (exact match)
            - Deserialization succeeds for valid exact types
            - No subclass tolerance

        Design Trade-off:
            Strict vs Permissive:
                - Strict: Homogeneous collections (predictable)
                - Permissive: Heterogeneous collections (flexible)
                - Strict prevents "surprising" subtypes

        Use Case:
            Task queues where all tasks must be exact type (not subclasses)
            to ensure uniform processing behavior.

        Complexity:
            Type check: O(1) set membership (type(x) in item_type)
        """
        # Create pile with exact type in strict mode
        exact_item = TypedElement(name="exact")
        pile = Pile(items=[exact_item], item_type=TypedElement, strict_type=True)
        data = pile.to_dict()

        # Should deserialize successfully
        restored = Pile.from_dict(data)
        assert len(restored) == 1
        assert restored.strict_type is True

    def test_to_dict_with_progression_name_and_db_mode(self):
        """
        Test progression name serialization with db mode and custom meta_key.

        Pattern:
            Mode-specific metadata handling (db vs python/json)

        Edge Case:
            Custom meta_key for database column naming conventions

        Scenario:
            1. Create pile with empty metadata
            2. Set progression name
            3. Serialize with db mode and custom meta_key
            4. Verify progression_name in custom metadata field

        Expected:
            - Metadata initialized as {} if None/empty
            - progression_name stored in custom meta_key field
            - db mode uses custom column names

        Design Rationale:
            Why mode-aware metadata?
                - python mode: Python objects (UUID, datetime)
                - json mode: JSON-safe strings
                - db mode: Database column names (node_metadata, pile_meta)

        Use Case:
            ORM integration where metadata column has custom name
            (e.g., "pile_meta" instead of "metadata").

        Complexity:
            Metadata initialization: O(1)
        """
        # Create pile with empty metadata
        elem = Element()
        pile = Pile([elem])

        # Set progression name
        pile._progression.name = "custom_progression"

        # Serialize with metadata renaming (db mode)
        data = pile.to_dict(mode="db", meta_key="pile_meta")

        # The progression_name should be stored in the metadata
        assert "pile_meta" in data
        assert data["pile_meta"]["progression_name"] == "custom_progression"

    def test_from_dict_with_runtime_type_objects(self):
        """
        Test from_dict with type objects (not strings).

        Pattern:
            Programmatic API with type objects vs serialized strings

        Edge Case:
            Passing type objects directly instead of string references

        Scenario:
            1. Create pile with type constraint
            2. Serialize to dict
            3. Deserialize with type object passed as kwarg
            4. Verify type constraint restored

        Expected:
            - extract_types() handles both strings and type objects
            - No import/lookup needed for type objects
            - Direct type reference works

        Design Rationale:
            Why support both?
                - Strings: Serialization format (cross-process)
                - Type objects: Programmatic usage (same process)
                - extract_types() unifies both paths

        Use Case:
            Programmatic pile construction where types are available
            as objects (not serialized strings).

        Complexity:
            Type object handling: O(1) (no import/lookup)
        """
        # Create pile with type constraint
        elem1 = Element()
        elem2 = Element()
        pile = Pile([elem1, elem2], item_type=Element)

        # Serialize to dict
        data = pile.to_dict(mode="python")

        # Modify data to simulate runtime case - pass actual type objects, not strings
        # This happens when constructing Pile programmatically with from_dict
        # and passing item_type directly as kwargs
        restored = Pile.from_dict(data, item_type=Element)

        assert len(restored) == 2
        assert restored.item_type == {Element}

    def test_from_dict_with_invalid_lion_class_skips_validation(self):
        """
        Test validation loop continues when lion_class can't be imported.

        Pattern:
            Fail-safe validation (don't crash on invalid metadata)

        Edge Case:
            Invalid lion_class in metadata (typo, missing module)

        Scenario:
            1. Create data with invalid and valid lion_class values
            2. Attempt deserialization
            3. Verify validation loop catches ValueError and continues
            4. Verify deserialization fails later (Element.from_dict)

        Expected:
            - Validation loop: Skip invalid lion_class, continue
            - Deserialization error: Later stage (not validation)
            - ValueError message: "Failed to deserialize"

        Design Rationale:
            Why separate validation and deserialization?
                - Validation: Check types are compatible (early)
                - Deserialization: Construct objects (late)
                - Graceful degradation: Don't crash on metadata issues

        Use Case:
            Deserializing data from external sources where metadata
            may be corrupted or outdated.

        Complexity:
            Validation: O(n) with try/except per item
        """
        # Create data with item_type constraint and one invalid, one valid lion_class
        # The validation loop should catch ValueError for invalid class and continue
        data = {
            "id": "550e8400-e29b-41d4-a716-446655440000",
            "created_at": "2025-01-01T00:00:00Z",
            "metadata": {},
            "items": [
                {
                    "id": "650e8400-e29b-41d4-a716-446655440001",
                    "created_at": "2025-01-01T00:00:00Z",
                    "metadata": {
                        "lion_class": "nonexistent.module.InvalidClass"  # Invalid - triggers ValueError
                    },
                },
                {
                    "id": "750e8400-e29b-41d4-a716-446655440002",
                    "created_at": "2025-01-01T00:00:00Z",
                    "metadata": {
                        "lion_class": "lionpride.core.element.Element"  # Valid
                    },
                },
            ],
            "item_type": ["lionpride.core.element.Element"],
            "strict_type": True,  # Enable validation to trigger the code path
        }

        # The validation should skip the invalid lion_class gracefully
        # and continue validating the valid one without raising
        # The actual deserialization will still fail for the invalid class, but that's later
        # We're testing that the VALIDATION loop handles ValueError correctly
        try:
            Pile.from_dict(data)
            # If we get here, the validation worked but deserialization failed later
        except ValueError as e:
            # The ValueError should be from Element.from_dict deserialization, not from validation
            # Verify it's the deserialization error (not validation error)
            assert "Failed to deserialize" in str(e)

    def test_from_dict_validation_with_all_invalid_lion_classes(self):
        """
        Test validation with all invalid lion_class values.

        Pattern:
            Extreme edge case - no valid metadata at all

        Edge Case:
            All items have invalid lion_class (worst-case scenario)

        Scenario:
            1. Create data with all invalid lion_class values
            2. Attempt deserialization
            3. Verify validation loop completes gracefully
            4. Verify deserialization fails appropriately

        Expected:
            - Validation: Completes without crashing
            - Deserialization: Fails (no valid items)
            - Graceful error handling throughout

        Design Rationale:
            Why continue validation?
                - Collect all errors (not just first)
                - Graceful degradation philosophy
                - Better error reporting

        Use Case:
            Bulk data import from legacy systems where all metadata
            may be outdated or malformed.

        Complexity:
            Validation: O(n) with try/except for each item
        """
        data = {
            "id": "550e8400-e29b-41d4-a716-446655440000",
            "created_at": "2025-01-01T00:00:00Z",
            "metadata": {},
            "items": [
                {
                    "id": "650e8400-e29b-41d4-a716-446655440001",
                    "created_at": "2025-01-01T00:00:00Z",
                    "metadata": {"lion_class": "invalid.one.Class"},
                },
                {
                    "id": "750e8400-e29b-41d4-a716-446655440002",
                    "created_at": "2025-01-01T00:00:00Z",
                    "metadata": {"lion_class": "invalid.two.Class"},
                },
            ],
            "item_type": ["lionpride.core.element.Element"],
            "strict_type": False,  # Permissive mode
        }

        # Validation should skip all invalid classes gracefully
        # Deserialization will fail, but validation loop should complete
        try:
            Pile.from_dict(data)
        except ValueError:
            # Expected - deserialization fails, but validation loop completed
            pass


# =============================================================================
# Collection Methods Tests
# =============================================================================


def test_contains_by_uuid(simple_items):
    """Test __contains__ with UUID."""
    pile = Pile(items=simple_items)
    assert simple_items[0].id in pile

    from uuid import uuid4

    assert uuid4() not in pile


def test_contains_by_element(simple_items):
    """Test __contains__ with Element instance."""
    pile = Pile(items=simple_items)
    assert simple_items[0] in pile
    assert TestElement(value=999) not in pile


def test_contains_invalid_returns_false():
    """Test __contains__ with invalid input returns False."""
    pile = Pile()
    assert "invalid" not in pile
    assert 42 not in pile


def test_len(simple_items):
    """Test __len__."""
    pile = Pile(items=simple_items)
    assert len(pile) == 5

    pile.remove(simple_items[0].id)
    assert len(pile) == 4


def test_iter(simple_items):
    """Test __iter__ yields items in progression order."""
    pile = Pile(items=simple_items)

    items_list = list(pile)
    assert len(items_list) == 5
    assert items_list == simple_items


def test_list_conversion(simple_items):
    """Test __list__ and to_list()."""
    pile = Pile(items=simple_items)

    # __list__
    items_list = pile.__list__()
    assert items_list == simple_items

    # list() uses __list__()
    items_list2 = list(pile)
    assert items_list2 == simple_items


def test_keys(simple_items):
    """Test keys() yields UUIDs."""
    pile = Pile(items=simple_items)
    keys_list = list(pile.keys())

    assert len(keys_list) == 5
    assert all(isinstance(k, UUID) for k in keys_list)
    assert all(k in [item.id for item in simple_items] for k in keys_list)


def test_values(simple_items):
    """Test __iter__ yields items (values)."""
    pile = Pile(items=simple_items)
    values_list = list(pile)  # __iter__ yields items

    assert len(values_list) == 5
    assert values_list == simple_items


def test_len_and_bool(simple_items):
    """Test __len__ and __bool__."""
    pile = Pile(items=simple_items)
    assert len(pile) == 5
    assert pile  # __bool__ returns True for non-empty pile


def test_is_empty():
    """Test is_empty()."""
    pile = Pile()
    assert pile.is_empty()
    assert not pile  # __bool__ returns False for empty pile

    pile.add(TestElement(value=42))
    assert not pile.is_empty()
    assert pile  # __bool__ returns True for non-empty pile

    pile.clear()
    assert pile.is_empty()


# =============================================================================
# Property Tests
# =============================================================================


def test_keys_method(simple_items):
    """Test keys() method returns UUID iterator."""
    pile = Pile(items=simple_items)
    keys_list = list(pile.keys())

    # Should return all UUIDs
    assert len(keys_list) == len(simple_items)
    assert all(item.id in keys_list for item in simple_items)

    # Should be in insertion order
    expected_order = [item.id for item in simple_items]
    assert keys_list == expected_order


def test_items_method(simple_items):
    """Test items() method returns (UUID, item) iterator."""
    pile = Pile(items=simple_items)
    items_list = list(pile.items())

    # Should return all (UUID, item) pairs
    assert len(items_list) == len(simple_items)

    # Each item should be a tuple
    for uuid, item in items_list:
        assert isinstance(uuid, UUID)
        assert pile[uuid] == item

    # Should be in insertion order
    expected_order = [(item.id, item) for item in simple_items]
    assert items_list == expected_order


def test_progression_property_copy(simple_items):
    """Test progression property returns copy."""
    pile = Pile(items=simple_items)
    prog1 = pile.progression
    prog2 = pile.progression

    # Should be different objects (copies)
    assert prog1 is not prog2
    assert list(prog1.order) == list(prog2.order)


# =============================================================================
# Edge Cases Tests
# =============================================================================


def test_empty_pile_operations():
    """Test operations on empty pile."""
    pile = Pile()

    # Iteration
    assert list(pile) == []
    assert list(pile.keys()) == []
    assert list(pile.items()) == []

    # Size checks
    assert len(pile) == 0
    assert pile.is_empty()
    assert not pile  # __bool__ returns False for empty pile

    # Clear on empty
    pile.clear()
    assert len(pile) == 0


def test_pile_repr():
    """Test __repr__."""
    pile = Pile()
    assert repr(pile) == "Pile(len=0)"

    pile.add(TestElement(value=42))
    assert repr(pile) == "Pile(len=1)"


def test_progression_order_integrity_after_operations(simple_items):
    """Test progression maintains order through operations."""
    pile = Pile(items=simple_items)

    # Add new item
    new_item = TestElement(value=999)
    pile.add(new_item)

    # Check order includes new item at end
    items_list = list(pile)
    assert items_list == [*simple_items, new_item]

    # Remove middle item
    pile.remove(simple_items[2].id)

    # Check order maintained (without removed item)
    expected = simple_items[:2] + simple_items[3:] + [new_item]
    assert list(pile) == expected


@pytest.mark.asyncio
async def test_async_operations_with_type_validation():
    """Test async operations respect type validation."""
    pile = Pile(item_type=TypedElement)

    valid_item = TypedElement(name="valid")
    async with pile:
        pile.add(valid_item)

    assert len(pile) == 1

    invalid_item = TestElement(value=42)
    async with pile:
        with pytest.raises(TypeError, match="not a subclass"):
            pile.add(invalid_item)


def test_filter_operations_preserve_config(simple_items):
    """Test filter operations preserve item_type and strict_type."""
    pile = Pile(
        items=simple_items,
        item_type=TestElement,
        strict_type=True,
    )

    # Filter by callable
    filtered = pile[lambda x: x.value >= 3]
    assert filtered.item_type == pile.item_type
    assert filtered.strict_type == pile.strict_type

    # Filter by progression
    prog = Progression(order=[simple_items[0].id, simple_items[1].id])
    filtered2 = pile[prog]
    assert filtered2.item_type == pile.item_type
    assert filtered2.strict_type == pile.strict_type


# ==================== Coverage Tests for Missing Lines ====================


def test_pile_to_dict_with_name_and_none_meta(simple_items):
    """Test Pile.to_dict with name and meta_key=None path (line 202)."""
    pile = Pile[Element](items=simple_items[:2])
    # Give the progression a name
    pile._progression.name = "test_pile"

    # Serialize with mode that creates None meta dict
    result = pile.to_dict(mode="db", meta_key="meta")

    # Should have created meta dict and added progression_name
    assert "meta" in result
    assert result["meta"] is not None
    assert result["meta"]["progression_name"] == "test_pile"


def test_pile_getitem_progression_with_missing_uuid(simple_items):
    """Test pile[progression] raises NotFoundError for missing UUID (line 449)."""
    from lionpride.errors import NotFoundError

    pile = Pile[Element](items=simple_items[:2])

    # Create progression with a UUID not in pile
    from uuid import uuid4

    missing_uuid = uuid4()
    prog = Progression(order=[simple_items[0].id, missing_uuid])

    # Should raise NotFoundError
    with pytest.raises(NotFoundError) as exc_info:
        _ = pile[prog]

    assert "not found in pile" in str(exc_info.value)


def test_pile_from_dict_runtime_extract_types(simple_items):
    """Test Pile.from_dict with item_type at runtime (line 667)."""
    # Create a Pile and serialize it
    pile = Pile[Element](items=simple_items[:2])
    pile_dict = pile.to_dict()

    # Add item_type as a type object (not string) - forces runtime extract_types path
    pile_dict["item_type"] = [Element]  # Type object, not string

    # Deserialize - should use extract_types for runtime case
    restored = Pile.from_dict(pile_dict)

    assert len(restored) == 2
    assert restored.item_type == {Element}  # extract_types normalizes to set


def test_pile_exclude_progression_already_removed():
    """Test exclude when uid already removed from progression.

    Coverage: pile.py lines 360-361 (except ValueError: pass)

    Edge Case:
        Item in _items but already removed from _progression.
        This can happen in concurrent scenarios or internal state inconsistency.

    Scenario:
        1. Create pile with items
        2. Directly manipulate _progression to remove UUID (simulate state inconsistency)
        3. Call exclude() - should handle missing progression entry gracefully
        4. Verify exclude still removes from _items and returns True

    Expected:
        - exclude() returns True (absence guaranteed)
        - Item removed from _items
        - No ValueError raised (caught internally)
    """
    pile = Pile()
    item = TestElement(value=42)
    pile.add(item)

    # Verify item is in both _items and _progression
    assert item.id in pile._items
    assert item.id in pile._progression

    # Directly remove from progression to simulate state inconsistency
    # (This simulates a race condition or internal state corruption scenario)
    pile._progression.remove(item.id)

    # Now item is in _items but NOT in _progression
    assert item.id in pile._items
    assert item.id not in pile._progression

    # exclude() should handle this gracefully (lines 360-361: except ValueError: pass)
    result = pile.exclude(item.id)

    # Should return True (absence guaranteed) and remove from _items
    assert result is True
    assert item.id not in pile._items
    assert len(pile) == 0
