"""
Hypothesis Strategies for SST-Driven Property-Based Testing.

This module provides Hypothesis strategies that generate test data FROM
the YAML SST specifications, not from the implementation.

PHILOSOPHY:
    Tests written by humans tend to test what they THINK should work.
    Hypothesis tests what ACTUALLY works by generating thousands of edge cases.

KEY INSIGHT:
    Your YAML specs define what's valid. Hypothesis should generate:
    1. All valid combinations (should pass)
    2. All invalid variations (should fail)
    3. Boundary conditions you didn't think of

Usage:
    from hypothesis import given
    from lightwave.schema.testing.hypothesis_strategies import (
        valid_field_option,
        invalid_field_option,
        valid_status,
        any_pydantic_instance,
    )

    @given(valid_field_option("page_statuses"))
    def test_all_page_statuses_accepted(status):
        '''Every status in YAML should be accepted.'''
        PageSchema(status=status)  # Should not raise

    @given(invalid_field_option("page_statuses"))
    def test_invalid_statuses_rejected(bad_status):
        '''Anything NOT in YAML should be rejected.'''
        with pytest.raises(ValidationError):
            PageSchema(status=bad_status)
"""

from __future__ import annotations

import string
from typing import TYPE_CHECKING, Any, TypeVar

if TYPE_CHECKING:
    from pydantic import BaseModel

# Hypothesis is optional - only import if available
try:
    from hypothesis import strategies as st
    from hypothesis.strategies import SearchStrategy

    HAS_HYPOTHESIS = True
except ImportError:
    HAS_HYPOTHESIS = False
    st = None  # type: ignore
    SearchStrategy = Any  # type: ignore

T = TypeVar("T", bound="BaseModel")


def _require_hypothesis() -> None:
    """Raise if hypothesis is not installed."""
    if not HAS_HYPOTHESIS:
        raise ImportError("hypothesis is required for property-based testing. " "Install with: pip install hypothesis")


# =============================================================================
# SST-Driven Strategies
# =============================================================================


def valid_field_option(field_key: str) -> "SearchStrategy[str]":
    """
    Generate valid values for a field_options key from YAML.

    Reads from field_options.yaml (merged) and returns a strategy
    that generates any valid option.

    Usage:
        @given(valid_field_option("page_statuses"))
        def test_all_statuses_valid(status):
            assert status in ["draft", "published", "archived"]
    """
    _require_hypothesis()
    from lightwave.schema.pydantic.validators import get_field_values

    values = get_field_values(field_key)
    if not values:
        raise ValueError(f"No values found for field_key: {field_key}")
    return st.sampled_from(values)


def invalid_field_option(
    field_key: str,
    include_empty: bool = True,
    include_case_variants: bool = True,
    include_typos: bool = True,
) -> "SearchStrategy[str]":
    """
    Generate INVALID values for a field_options key.

    This is the adversarial counterpart to valid_field_option.
    Generates values that SHOULD be rejected.

    Args:
        field_key: The field_options key to generate invalid values for
        include_empty: Include empty string as invalid
        include_case_variants: Include case variations (Draft, DRAFT, dRaFt)
        include_typos: Include common typos (drft, draf, etc.)
    """
    _require_hypothesis()
    from lightwave.schema.pydantic.validators import get_field_values

    valid_values = set(get_field_values(field_key))

    # Build list of invalid strategies
    strategies: list[SearchStrategy[str]] = []

    # Obviously invalid
    strategies.append(st.just("__INVALID__"))
    strategies.append(st.just("INVALID_VALUE_12345"))

    if include_empty:
        strategies.append(st.just(""))

    if include_case_variants:
        # Case variants of valid values (if they're lowercase)
        for val in valid_values:
            if val.lower() == val:  # Originally lowercase
                strategies.append(st.just(val.upper()))
                strategies.append(st.just(val.title()))
                strategies.append(st.just(val.capitalize()))

    if include_typos:
        # Common typos: remove last char, swap chars, add extra char
        for val in valid_values:
            if len(val) > 2:
                strategies.append(st.just(val[:-1]))  # Missing last char
                strategies.append(st.just(val + "x"))  # Extra char
            if len(val) > 3:
                # Swap middle chars
                mid = len(val) // 2
                typo = val[:mid] + val[mid + 1] + val[mid] + val[mid + 2 :]
                if typo not in valid_values:
                    strategies.append(st.just(typo))

    # Random strings that aren't valid
    strategies.append(
        st.text(alphabet=string.ascii_lowercase, min_size=1, max_size=20).filter(lambda x: x not in valid_values)
    )

    return st.one_of(*strategies)


def all_field_options() -> "SearchStrategy[tuple[str, str]]":
    """
    Generate (field_key, valid_value) pairs for ALL field options.

    Useful for exhaustive testing across the entire SST.

    Usage:
        @given(all_field_options())
        def test_all_options_have_labels(key_value):
            key, value = key_value
            # Verify every option has a label in YAML
    """
    _require_hypothesis()
    from lightwave.schema.pydantic.validators import (
        get_all_field_option_keys,
        get_field_values,
    )

    # Get all keys
    keys = get_all_field_option_keys()
    if not keys:
        raise ValueError("No field option keys found in YAML")

    # Build strategy that picks a key, then picks a value from that key
    def key_value_pair(key: str) -> SearchStrategy[tuple[str, str]]:
        values = get_field_values(key)
        if values:
            return st.tuples(st.just(key), st.sampled_from(values))
        return st.nothing()  # Skip empty keys

    return st.sampled_from(keys).flatmap(key_value_pair)


# =============================================================================
# Pydantic Model Strategies
# =============================================================================


def pydantic_instance(
    model_class: type[T],
    overrides: dict[str, Any] | None = None,
) -> "SearchStrategy[T]":
    """
    Generate valid instances of a Pydantic model.

    Uses the model's field definitions to generate appropriate values.
    For Literal fields, picks from the allowed values.
    For string fields with constraints, respects them.

    Args:
        model_class: The Pydantic model class
        overrides: Dict of field_name -> value or strategy to override defaults

    Usage:
        @given(pydantic_instance(UserSchema))
        def test_all_valid_users_serialize(user):
            json_str = user.model_dump_json()
            assert UserSchema.model_validate_json(json_str) == user
    """
    _require_hypothesis()
    from typing import Literal, get_args, get_origin

    overrides = overrides or {}
    field_strategies: dict[str, SearchStrategy[Any]] = {}

    for field_name, field_info in model_class.model_fields.items():
        # Use override if provided
        if field_name in overrides:
            override = overrides[field_name]
            if isinstance(override, SearchStrategy):
                field_strategies[field_name] = override
            else:
                field_strategies[field_name] = st.just(override)
            continue

        annotation = field_info.annotation
        origin = get_origin(annotation)

        # Handle Literal types
        if origin is Literal:
            valid_values = get_args(annotation)
            field_strategies[field_name] = st.sampled_from(valid_values)
            continue

        # Handle basic types
        if annotation is str:
            max_length = getattr(field_info, "max_length", None) or 100
            min_length = getattr(field_info, "min_length", None) or 0
            field_strategies[field_name] = st.text(min_size=min_length, max_size=max_length)
        elif annotation is int:
            field_strategies[field_name] = st.integers(min_value=-1000000, max_value=1000000)
        elif annotation is float:
            field_strategies[field_name] = st.floats(allow_nan=False, allow_infinity=False)
        elif annotation is bool:
            field_strategies[field_name] = st.booleans()
        elif annotation is dict or origin is dict:
            field_strategies[field_name] = st.dictionaries(
                keys=st.text(min_size=1, max_size=20),
                values=st.text(min_size=0, max_size=50),
                max_size=5,
            )
        elif annotation is list or origin is list:
            field_strategies[field_name] = st.lists(st.text(min_size=1, max_size=20), max_size=5)
        else:
            # For complex types, use None if optional, or skip
            if not field_info.is_required():
                field_strategies[field_name] = st.none()
            # else: will fail if required and we can't generate

    @st.composite
    def build_instance(draw: Any) -> T:
        kwargs = {}
        for field_name, strategy in field_strategies.items():
            kwargs[field_name] = draw(strategy)
        return model_class(**kwargs)

    return build_instance()


def invalid_pydantic_instance(
    model_class: type[T],
    valid_base: dict[str, Any],
    field_to_break: str,
) -> "SearchStrategy[dict[str, Any]]":
    """
    Generate invalid instances by breaking a specific field.

    Returns a strategy of dicts (not model instances) since they should
    fail validation.

    Args:
        model_class: The Pydantic model class
        valid_base: A valid dict of field values
        field_to_break: Which field to make invalid

    Usage:
        @given(invalid_pydantic_instance(UserSchema, valid_user, "email"))
        def test_invalid_emails_rejected(invalid_data):
            with pytest.raises(ValidationError):
                UserSchema(**invalid_data)
    """
    _require_hypothesis()
    from typing import Literal, get_args, get_origin

    field_info = model_class.model_fields.get(field_to_break)
    if not field_info:
        raise ValueError(f"Field '{field_to_break}' not found in {model_class.__name__}")

    annotation = field_info.annotation
    origin = get_origin(annotation)

    # Generate invalid values based on field type
    if origin is Literal:
        valid_values = set(get_args(annotation))
        invalid_strategy = st.text(min_size=1, max_size=20).filter(lambda x: x not in valid_values)
    elif annotation is str:
        # For strings, use wrong types
        invalid_strategy = st.one_of(
            st.integers(),
            st.lists(st.integers(), min_size=1),
            st.dictionaries(st.text(), st.text(), min_size=1),
        )
    elif annotation is int:
        invalid_strategy = st.one_of(
            st.text(min_size=1),
            st.lists(st.integers(), min_size=1),
        )
    else:
        # Generic: use clearly wrong types
        invalid_strategy = st.one_of(
            st.just(object()),  # Random object
            st.just(lambda: None),  # Function
        )

    @st.composite
    def build_invalid(draw: Any) -> dict[str, Any]:
        result = valid_base.copy()
        result[field_to_break] = draw(invalid_strategy)
        return result

    return build_invalid()


# =============================================================================
# Boundary Testing Strategies
# =============================================================================


def at_boundary(
    field_name: str,
    max_length: int,
    valid_base: dict[str, Any],
) -> "SearchStrategy[dict[str, Any]]":
    """
    Generate values exactly AT a boundary.

    Usage:
        @given(at_boundary("content", 10000, valid_msg))
        def test_at_max_length_accepted(data):
            MessageSchema(**data)  # Should not raise
    """
    _require_hypothesis()

    @st.composite
    def build_at_boundary(draw: Any) -> dict[str, Any]:
        result = valid_base.copy()
        result[field_name] = "x" * max_length
        return result

    return build_at_boundary()


def beyond_boundary(
    field_name: str,
    max_length: int,
    valid_base: dict[str, Any],
    overflow_amount: int = 1,
) -> "SearchStrategy[dict[str, Any]]":
    """
    Generate values BEYOND a boundary.

    Usage:
        @given(beyond_boundary("content", 10000, valid_msg))
        def test_beyond_max_rejected(data):
            with pytest.raises(ValidationError):
                MessageSchema(**data)
    """
    _require_hypothesis()

    @st.composite
    def build_beyond(draw: Any) -> dict[str, Any]:
        result = valid_base.copy()
        # Generate varying amounts beyond the boundary
        extra = draw(st.integers(min_value=overflow_amount, max_value=overflow_amount + 100))
        result[field_name] = "x" * (max_length + extra)
        return result

    return build_beyond()


# =============================================================================
# SST Alignment Verification
# =============================================================================


def sst_alignment_data(yaml_key: str) -> "SearchStrategy[tuple[str, list[str]]]":
    """
    Generate data for SST alignment verification.

    Returns (yaml_key, yaml_values) tuples for verifying implementations
    match YAML specs.

    Usage:
        @given(sst_alignment_data("page_statuses"))
        def test_model_choices_match_yaml(key, yaml_values):
            model_choices = get_model_choices(Page, "status")
            assert set(model_choices) == set(yaml_values)
    """
    _require_hypothesis()
    from lightwave.schema.pydantic.validators import get_field_values

    values = get_field_values(yaml_key)
    return st.just((yaml_key, values))


# =============================================================================
# Composite Strategies for Complex Scenarios
# =============================================================================


@st.composite  # type: ignore[misc]
def tenant_isolated_data(
    draw: Any,
    model_factory: Any,  # Would be a model_bakery or factory_boy factory
) -> tuple[Any, Any, Any]:
    """
    Generate data for tenant isolation testing.

    Creates two tenants and data that should be isolated.

    Usage:
        @given(tenant_isolated_data(UserFactory))
        def test_tenant_isolation(tenant_a, tenant_b, data):
            # Data from tenant_a should not be visible in tenant_b
    """
    if not HAS_HYPOTHESIS:
        raise ImportError("hypothesis required")

    # This is a template - actual implementation depends on your tenant setup
    # The key insight is: generate data that SHOULD be isolated, then verify it is

    tenant_a_id = draw(st.uuids())
    tenant_b_id = draw(st.uuids())

    # Generate some data for tenant_a
    data_count = draw(st.integers(min_value=1, max_value=5))

    return (tenant_a_id, tenant_b_id, data_count)


# =============================================================================
# Exported Strategies
# =============================================================================

__all__ = [
    "HAS_HYPOTHESIS",
    "valid_field_option",
    "invalid_field_option",
    "all_field_options",
    "pydantic_instance",
    "invalid_pydantic_instance",
    "at_boundary",
    "beyond_boundary",
    "sst_alignment_data",
    "tenant_isolated_data",
]
