"""Property-based tests using Hypothesis strategies."""

from __future__ import annotations

import hypothesis.strategies as st
from hypothesis import assume, given, settings

from tests.strategies import (
    error_dicts,
    error_messages,
    exception_types,
    search_queries,
    stack_traces,
)


class TestErrorStrategies:
    """Test that our custom strategies generate valid data."""

    @given(error_messages())
    @settings(max_examples=50)
    def test_error_messages_are_strings(self, msg):
        """Error messages should be non-empty strings."""
        assert isinstance(msg, str)
        assert len(msg) > 0

    @given(exception_types())
    @settings(max_examples=30)
    def test_exception_types_are_valid(self, exc_type):
        """Exception types should be valid exception names."""
        assert isinstance(exc_type, str)
        # Most exceptions end with "Error" but some don't (e.g., StopIteration)
        assert exc_type.endswith("Error") or exc_type.endswith("Iteration") or exc_type[0].isupper()

    @given(stack_traces())
    @settings(max_examples=30)
    def test_stack_traces_have_traceback_header(self, trace):
        """Stack traces should start with traceback header."""
        assert isinstance(trace, str)
        assert trace.startswith("Traceback (most recent call last):")

    @given(error_dicts())
    @settings(max_examples=30)
    def test_error_dicts_have_required_fields(self, error):
        """Error dicts should have all required fields."""
        assert isinstance(error, dict)
        assert "id" in error
        assert "exception_type" in error
        assert "message" in error
        assert "stack_trace" in error
        assert "file" in error
        assert "line" in error

    @given(error_dicts())
    @settings(max_examples=30)
    def test_error_dict_id_format(self, error):
        """Error IDs should have correct format."""
        assert error["id"].startswith("err_")
        assert len(error["id"]) == 10  # "err_" + 6 chars


class TestTextProcessing:
    """Property tests for text processing functions."""

    @given(error_dicts())
    @settings(max_examples=50)
    def test_embedding_text_is_deterministic(self, error):
        """Same error should produce same embedding text."""
        # Create embedding text twice
        text1 = f"{error['exception_type']}: {error['message']}"
        text2 = f"{error['exception_type']}: {error['message']}"

        assert text1 == text2

    @given(error_dicts())
    @settings(max_examples=50)
    def test_embedding_text_contains_exception_type(self, error):
        """Embedding text should contain exception type."""
        text = f"{error['exception_type']}: {error['message']}"

        assert error["exception_type"] in text

    @given(error_dicts())
    @settings(max_examples=50)
    def test_embedding_text_length_is_bounded(self, error):
        """Embedding text should not be excessively long."""
        text = f"{error['exception_type']}: {error['message']}\n{error['stack_trace']}"

        # OpenAI embedding model has ~8191 token limit
        # With average 4 chars/token, that's ~32K chars
        assert len(text) < 50000


class TestSearchQueries:
    """Property tests for search query processing."""

    @given(search_queries())
    @settings(max_examples=50)
    def test_search_queries_are_strings(self, query):
        """Search queries should be non-empty strings."""
        assert isinstance(query, str)
        assert len(query) > 0

    @given(search_queries())
    @settings(max_examples=50)
    def test_search_queries_are_printable(self, query):
        """Search queries should only contain printable characters."""
        # Allow some whitespace
        for char in query:
            assert char.isprintable() or char in (' ', '\t', '\n')


class TestRRFScoring:
    """Property tests for Reciprocal Rank Fusion calculations."""

    @given(st.integers(min_value=0, max_value=1000))
    def test_rrf_score_is_bounded(self, rank):
        """RRF scores should be in valid range."""
        k = 60  # Standard RRF constant
        rrf_score = 1 / (k + rank)

        assert 0 < rrf_score <= 1 / k

    @given(st.integers(min_value=0, max_value=1000))
    def test_rrf_monotonically_decreasing(self, rank):
        """Higher ranks should have lower RRF scores."""
        k = 60

        score_at_rank = 1 / (k + rank)
        score_at_next = 1 / (k + rank + 1)

        assert score_at_rank > score_at_next

    @given(
        st.lists(st.integers(min_value=0, max_value=100), min_size=1, max_size=10),
        st.lists(st.integers(min_value=0, max_value=100), min_size=1, max_size=10),
    )
    def test_rrf_combined_score(self, ranks1, ranks2):
        """Combined RRF scores should be bounded."""
        k = 60

        # Calculate combined RRF score for first item
        score1 = 1 / (k + ranks1[0]) if ranks1 else 0
        score2 = 1 / (k + ranks2[0]) if ranks2 else 0
        combined = score1 + score2

        # Combined score should be at most 2/k
        assert combined <= 2 / k + 0.001  # Small epsilon for float comparison


class TestCacheKeyGeneration:
    """Property tests for cache key generation."""

    @given(st.text(min_size=1, max_size=1000))
    def test_hash_is_deterministic(self, text):
        """Same text should produce same hash."""
        hash1 = hash(text)
        hash2 = hash(text)

        assert hash1 == hash2

    @given(
        st.text(min_size=1, max_size=100),
        st.text(min_size=1, max_size=100),
    )
    def test_different_texts_usually_different_hashes(self, text1, text2):
        """Different texts should usually have different hashes."""
        assume(text1 != text2)

        hash1 = hash(text1)
        hash2 = hash(text2)

        # Collisions are possible but rare
        # We don't assert inequality since collisions can happen
        # This test documents the expectation
        _ = hash1 != hash2  # Usually true

