"""Tests for ChainContextLayer inheritance and strict typing.

This module tests the question: "Can we inherit ChainContextLayer with strict typing?"
It demonstrates both the technical possibility and the design intent conflicts.
"""

import pytest

from dataclasses import dataclass

from crystflux.v1.boundary.chain_context import ChainContext, ChainContextLayer
from crystflux.v1.adapters.base_chain_vesicle import EmptyChainContext


# ============================================================================
# Pattern 1: Direct inheritance with type narrowing (Liskov Substitution Violation)
# ============================================================================

@dataclass(frozen=True)
class IntStrictLayer(ChainContextLayer):
    """Attempts to narrow payload type to int via inheritance.

    This is technically allowed by Python syntax but violates Liskov Substitution Principle.
    Static type checkers will warn about this.
    """
    payload: int  # Type narrowing from object to int


@dataclass(frozen=True)
class DictStrictLayer(ChainContextLayer):
    """Attempts to narrow payload type to dict via inheritance.

    Same LSP violation issue as IntStrictLayer.
    """
    payload: dict[str, object]


class TestStrictInheritanceLSPViolation:
    """Tests showing Liskov Substitution Principle violation."""

    def test_int_strict_layer_accepts_int(self) -> None:
        """IntStrictLayer should accept int values."""
        layer = IntStrictLayer(tag="count", payload=42)
        assert layer.tag == "count"
        assert layer.payload == 42

    def test_int_strict_layer_rejects_non_int(self) -> None:
        """IntStrictLayer should reject non-int values at static type level.

        Note: This is a static type error only. Runtime may allow it depending on
        how the layer is constructed.
        """
        # This would be a static type error with pyright/mypy
        # layer = IntStrictLayer(tag="count", payload="hello")
        pass  # Test documents the expected static type error

    def test_dict_strict_layer_accepts_dict(self) -> None:
        """DictStrictLayer should accept dict values."""
        layer = DictStrictLayer(tag="filter", payload={"key": "value"})
        assert layer.tag == "filter"
        assert layer.payload == {"key": "value"}

    def test_strict_layers_can_coexist_in_context(self) -> None:
        """Strict layers can be stored together in ChainContext.layers.

        However, the context's type remains tuple[ChainContextLayer, ...]
        which loses the specific type information.
        """
        int_layer = IntStrictLayer(tag="count", payload=10)
        dict_layer = DictStrictLayer(tag="filter", payload={"status": "active"})

        context = EmptyChainContext(layers=(int_layer, dict_layer))

        assert len(context.layers) == 2
        assert isinstance(context.layers[0], IntStrictLayer)
        assert isinstance(context.layers[1], DictStrictLayer)

    def test_lsp_violation_return_type_issue(self) -> None:
        """Demonstrate LSP violation when function returns ChainContextLayer.

        A function returning ChainContextLayer can return IntStrictLayer,
        but callers expecting object payload get a more restrictive type.
        """
        def create_layer(payload: object) -> ChainContextLayer:
            # Static type checker may complain about this
            return IntStrictLayer(tag="convert", payload=payload)  # type: ignore[arg-type]

        # This works for valid payloads
        layer = create_layer(5)
        assert isinstance(layer, IntStrictLayer)
        assert layer.payload == 5


# ============================================================================
# Pattern 2: Inheritance with runtime validation (still LSP violation but safer)
# ============================================================================

@dataclass(frozen=True)
class ValidatedIntLayer(ChainContextLayer):
    """Validated strict layer that checks payload type at runtime with __post_init__."""
    tag: str
    payload: object
    
    def __post_init__(self) -> None:
        if not isinstance(self.payload, int):
            raise TypeError(f"ValidatedIntLayer requires int payload, got {type(self.payload)}")
    
    def as_int(self) -> int:
        """Return payload as int (guaranteed by __post_init__)."""
        return self.payload  # type: ignore[return-value]


@dataclass(frozen=True)
class ValidatedSecretLayer(ChainContextLayer):
    """Validated strict layer for sensitive data.

    Demonstrates that validation can be more complex than just type checking.
    """
    tag: str
    payload: object

    def __post_init__(self) -> None:
        if not isinstance(self.payload, (bytes, str)):
            payload_type = type(self.payload).__name__
            raise TypeError(
                f"ValidatedSecretLayer requires bytes or str payload, got {payload_type}"
            )
        if len(self.payload) == 0:  # type: ignore[arg-type]
            raise ValueError("ValidatedSecretLayer payload cannot be empty")
    
    def as_secret(self) -> bytes | str:
        """Return payload as bytes or str (guaranteed by __post_init__)."""
        return self.payload  # type: ignore[return-value]


class TestValidatedInheritance:
    """Tests for runtime-validated strict layers."""

    def test_validated_int_layer_accepts_valid_payload(self) -> None:
        """Should accept valid int payload."""
        layer = ValidatedIntLayer(tag="count", payload=100)
        assert layer.as_int() == 100

    def test_validated_int_layer_rejects_invalid_payload(self) -> None:
        """Should reject non-int payload at runtime."""
        with pytest.raises(TypeError, match="requires int payload"):
            ValidatedIntLayer(tag="count", payload="not an int")

    def test_validated_secret_layer_accepts_bytes(self) -> None:
        """Should accept bytes payload."""
        layer = ValidatedSecretLayer(tag="api_key", payload=b"secret_key")
        assert layer.as_secret() == b"secret_key"

    def test_validated_secret_layer_rejects_empty_payload(self) -> None:
        """Should reject empty payload."""
        with pytest.raises(ValueError, match="cannot be empty"):
            ValidatedSecretLayer(tag="api_key", payload=b"")

    def test_validated_layers_in_chain_context(self) -> None:
        """Validated layers should work seamlessly with ChainContext."""
        int_layer = ValidatedIntLayer(tag="limit", payload=50)
        secret_layer = ValidatedSecretLayer(tag="token", payload="abc123")

        context = EmptyChainContext()
        context = context.evolve(int_layer)
        context = context.evolve(secret_layer)
        
        assert len(context.layers) == 2
        assert isinstance(context.layers[0], ValidatedIntLayer)
        assert context.layers[0].as_int() == 50
        assert isinstance(context.layers[1], ValidatedSecretLayer)
        assert context.layers[1].as_secret() == "abc123"

    def test_evolve_preserves_immutability(self) -> None:
        """evolve should return a new instance, not modify the original."""
        original = EmptyChainContext()
        layer = ValidatedIntLayer(tag="count", payload=1)

        evolved = original.evolve(layer)

        assert original.layers == ()  # Original remains unchanged
        assert evolved.layers == (layer,)  # New instance has the layer
        assert original is not evolved


# ============================================================================
# Pattern 3: Constructor helpers (Recommended approach)
# ============================================================================

@dataclass(frozen=True)
class CounterLayer(ChainContextLayer):
    """Constructor helper for counter-valued layers.
    
    Maintains object type in payload but ensures it's int via validation.
    """
    tag: str
    payload: object
    
    def __post_init__(self) -> None:
        if not isinstance(self.payload, int):
            raise TypeError("CounterLayer requires int payload")
        if self.payload < 0:
            raise ValueError(f"CounterLayer requires non-negative int, got {self.payload}")
    
    def as_int(self) -> int:
        """Return payload as int (guaranteed by __post_init__)."""
        return self.payload  # type: ignore[return-value]


@dataclass(frozen=True)
class FilterLayer(ChainContextLayer):
    """Constructor helper for filter condition layers.
    
    Validates that payload is a dict with string keys.
    """
    tag: str
    payload: object
    
    def __post_init__(self) -> None:
        if not isinstance(self.payload, dict):
            raise TypeError("FilterLayer requires dict payload")
        if not all(isinstance(k, str) for k in self.payload):  # type: ignore[assignment]
            raise TypeError("FilterLayer requires string keys in dict")
    
    def as_dict(self) -> dict[str, object]:
        """Return a shallow copy of the payload as dict.
        
        This allows the caller to safely modify the returned dict
        without affecting the original layer's payload.
        """
        return dict(self.payload)  # type: ignore[return-value, call-overload]


class TestConstructorHelpers:
    """Tests for constructor helper pattern (recommended)."""

    def test_counter_layer_validates_non_negative_int(self) -> None:
        """Should accept non-negative integers."""
        layer = CounterLayer(tag="page_offset", payload=0)
        assert layer.payload == 0

    def test_counter_layer_rejects_negative_int(self) -> None:
        """Should reject negative integers."""
        with pytest.raises(ValueError, match="non-negative"):
            CounterLayer(tag="page_offset", payload=-1)

    def test_filter_layer_validates_structure(self) -> None:
        """Should validate both type and key types."""
        layer = FilterLayer(tag="search", payload={"status": "active", "age_gt": 30})
        assert layer.payload == {"status": "active", "age_gt": 30}

    def test_filter_layer_rejects_non_string_keys(self) -> None:
        """Should reject dictionaries with non-string keys."""
        with pytest.raises(TypeError, match="string keys"):
            FilterLayer(tag="invalid", payload={123: "value"})

    def test_constructor_helpers_chain_cleanly(self) -> None:
        """Multiple constructor helpers should chain together cleanly."""
        context = EmptyChainContext()

        # Add filter conditions
        context = context.evolve(FilterLayer(tag="filter1", payload={"active": True}))
        context = context.evolve(FilterLayer(tag="filter2", payload={"age_gte": 18}))

        # Add counter
        context = context.evolve(CounterLayer(tag="limit", payload=100))

        assert len(context.layers) == 3
        assert sum(isinstance(layer, FilterLayer) for layer in context.layers) == 2
        assert sum(isinstance(layer, CounterLayer) for layer in context.layers) == 1


# ============================================================================
# Pattern 4: Mixed contexts and type narrowing
# ============================================================================

class TestMixedContextsAndTypeNarrowing:
    """Tests for handling mixed layers and runtime type narrowing."""

    def test_type_narrowing_with_isinstance(self) -> None:
        """Demonstrate safe type narrowing using isinstance checks."""
        int_layer = CounterLayer(tag="count", payload=42)
        dict_layer = FilterLayer(tag="filter", payload={"key": "value"})

        context = EmptyChainContext(layers=(int_layer, dict_layer))

        results: list[str] = []
        for layer in context.layers:
            if isinstance(layer, CounterLayer):
                # Use accessor to get typed value
                count = layer.as_int()
                results.append(f"Counter: {count * 2}")
            elif isinstance(layer, FilterLayer):
                # Use accessor to get typed value
                filters = layer.as_dict()
                results.append(f"Filter: {list(filters.keys())}")

        assert results == ["Counter: 84", "Filter: ['key']"]

    def test_context_evolution_preserves_layer_types(self) -> None:
        """evolve should preserve the concrete types of layers."""
        initial = EmptyChainContext()

        layer1 = CounterLayer(tag="a", payload=1)
        layer2 = FilterLayer(tag="b", payload={"x": 1})

        evolved = initial.evolve(layer1).evolve(layer2).evolve(CounterLayer(tag="c", payload=2))

        assert len(evolved.layers) == 3
        assert isinstance(evolved.layers[0], CounterLayer)
        assert isinstance(evolved.layers[1], FilterLayer)
        assert isinstance(evolved.layers[2], CounterLayer)

    def test_tag_based_filtering_works(self) -> None:
        """Layers can be filtered by tag regardless of concrete type."""
        layers = (
            CounterLayer(tag="page", payload=1),
            FilterLayer(tag="search", payload={"q": "test"}),
            CounterLayer(tag="size", payload=100),
        )
        context = EmptyChainContext(layers=layers)

        # Get all CounterLayers
        counters = [layer for layer in context.layers if isinstance(layer, CounterLayer)]
        assert len(counters) == 2

        # Get by tag prefix
        search_layers = [layer for layer in context.layers if layer.tag.startswith("se")]
        assert len(search_layers) == 1
        assert isinstance(search_layers[0], FilterLayer)


# ============================================================================
# Integration tests: demonstrating design intent vs technical possibility
# ============================================================================

class TestDesignIntentSummary:
    """Summary tests highlighting the tension between design intent and technical possibility."""

    def test_strict_typing_is_technically_possible(self) -> None:
        """Strict typing via inheritance is technically possible in Python."""
        # This compiles and runs
        strict = IntStrictLayer(tag="test", payload=123)
        assert strict.payload == 123

    def test_design_intent_requires_opacity(self) -> None:
        """Design intent requires payload to remain opaque at boundary level.

        The boundary layer should not interpret payload, meaning:
        - Type annotations remain as `object`
        - Validation happens in constructor helpers, not the base class
        - Interpretation is deferred to adapter layer
        """
        # Base class keeps object type
        base = ChainContextLayer(tag="any", payload=object())
        assert isinstance(base.payload, object)

        # Validation happens in specific implementations
        validated = ValidatedIntLayer(tag="validated", payload=456)
        # But the type annotation remains object externally
        assert isinstance(validated.payload, object)

    def test_conclusion_prefer_constructor_helpers(self) -> None:
        """Constructor helpers maintain design intent while providing type safety.

        This test demonstrates the recommended approach:
        1. Keep payload as object in type annotation
        2. Add runtime validation in __post_init__
        3. Use isinstance checks for type narrowing at use site
        4. Preserve "interpretation deferral" principle
        """
        context = EmptyChainContext()

        # Use constructor helpers for type safety
        context = context.evolve(CounterLayer(tag="limit", payload=50))
        context = context.evolve(FilterLayer(tag="where", payload={"active": True}))

        # Design intent: interpretation is deferred to the consumer
        def process_context(ctx: ChainContext) -> dict[str, int | None | dict[str, object]]:
            result: dict[str, int | None | dict[str, object]] = {"limit": None, "filters": {}}

            for layer in ctx.layers:
                if isinstance(layer, CounterLayer):
                    result["limit"] = layer.as_int()
                elif isinstance(layer, FilterLayer):
                    filters = result["filters"]
                    if isinstance(filters, dict):
                        filters.update(layer.as_dict())

            return result

        output = process_context(context)
        # Use individual assertions to clarify the None transition
        assert output["limit"] == 50
        assert output["filters"] == {"active": True}
