#!/usr/bin/env python3
"""
Property-based tests for validators using Hypothesis.

This module contains property-based tests that verify universal properties
of the validation functions across a wide range of inputs.
"""

from datetime import date
from hypothesis import given, strategies as st, settings, assume

from core.exceptions import DateRangeError, ValidationError
from utils.validators import (
    MIN_YEAR,
    MAX_YEAR,
    validate_date,
    validate_date_range,
    validate_time_index,
)


# ============================================================================
# Hypothesis Strategies
# ============================================================================

# Strategy for valid dates within the supported range (1900-2100)
valid_dates_in_range = st.dates(
    min_value=date(MIN_YEAR, 1, 1),
    max_value=date(MAX_YEAR, 12, 31)
)

# Strategy for dates before the supported range
dates_before_range = st.dates(
    min_value=date(1, 1, 1),
    max_value=date(MIN_YEAR - 1, 12, 31)
)

# Strategy for dates after the supported range
dates_after_range = st.dates(
    min_value=date(MAX_YEAR + 1, 1, 1),
    max_value=date(9999, 12, 31)
)

# Strategy for valid time indices (0-12)
valid_time_indices = st.integers(min_value=0, max_value=12)

# Strategy for invalid time indices (outside 0-12)
invalid_time_indices_low = st.integers(max_value=-1)
invalid_time_indices_high = st.integers(min_value=13)

# Strategy for leap years within range
leap_years_in_range = st.sampled_from([
    y for y in range(MIN_YEAR, MAX_YEAR + 1)
    if (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0)
])

# Strategy for non-leap years within range
non_leap_years_in_range = st.sampled_from([
    y for y in range(MIN_YEAR, MAX_YEAR + 1)
    if not ((y % 4 == 0 and y % 100 != 0) or (y % 400 == 0))
])


# ============================================================================
# Property Tests for Date Validation
# ============================================================================

class TestDateValidationProperties:
    """Property-based tests for date validation functions."""

    @given(d=valid_dates_in_range)
    @settings(max_examples=100)
    def test_valid_dates_in_range_accepted(self, d: date):
        """
        **Feature: code-quality-improvements, Property 1: Valid dates in range are accepted**
        
        For any date in YYYY-MM-DD format where year is between 1900 and 2100
        (inclusive), and the date is a valid calendar date, the validation
        function should accept it without raising an error.
        
        **Validates: Requirements 4.1**
        """
        date_str = d.strftime("%Y-%m-%d")
        # validate_date should return True
        assert validate_date(date_str) is True
        # validate_date_range should not raise
        validate_date_range(date_str)

    @given(d=dates_before_range)
    @settings(max_examples=100)
    def test_dates_before_range_rejected(self, d: date):
        """
        **Feature: code-quality-improvements, Property 2: Invalid dates outside range are rejected**
        
        For any date where the year is less than 1900, the validation function
        should raise a DateRangeError.
        
        **Validates: Requirements 3.2**
        """
        date_str = d.strftime("%Y-%m-%d")
        try:
            validate_date_range(date_str)
            assert False, f"Expected DateRangeError for date {date_str}"
        except DateRangeError:
            pass  # Expected

    @given(d=dates_after_range)
    @settings(max_examples=100)
    def test_dates_after_range_rejected(self, d: date):
        """
        **Feature: code-quality-improvements, Property 2: Invalid dates outside range are rejected**
        
        For any date where the year is greater than 2100, the validation function
        should raise a DateRangeError.
        
        **Validates: Requirements 3.2**
        """
        date_str = d.strftime("%Y-%m-%d")
        try:
            validate_date_range(date_str)
            assert False, f"Expected DateRangeError for date {date_str}"
        except DateRangeError:
            pass  # Expected


class TestLeapYearProperties:
    """Property-based tests for leap year validation."""

    @given(year=leap_years_in_range)
    @settings(max_examples=100)
    def test_leap_year_feb_29_accepted(self, year: int):
        """
        **Feature: code-quality-improvements, Property 6: Leap year February 29 validation**
        
        For any leap year (divisible by 4, except centuries not divisible by 400),
        February 29 should be accepted as a valid date.
        
        **Validates: Requirements 3.5**
        """
        date_str = f"{year:04d}-02-29"
        assert validate_date(date_str) is True
        # Should not raise
        validate_date_range(date_str)

    @given(year=non_leap_years_in_range)
    @settings(max_examples=100)
    def test_non_leap_year_feb_29_rejected(self, year: int):
        """
        **Feature: code-quality-improvements, Property 7: Non-leap year February 29 rejection**
        
        For any non-leap year, February 29 should be rejected with a ValidationError.
        
        **Validates: Requirements 3.6**
        """
        date_str = f"{year:04d}-02-29"
        # validate_date should return False for invalid calendar date
        assert validate_date(date_str) is False


# ============================================================================
# Property Tests for Time Index Validation
# ============================================================================

class TestTimeIndexProperties:
    """Property-based tests for time index validation."""

    @given(index=valid_time_indices)
    @settings(max_examples=100)
    def test_valid_time_indices_accepted(self, index: int):
        """
        For any time_index value from 0 to 12 (inclusive), the validation
        function should return True.
        
        **Validates: Requirements 3.3**
        """
        assert validate_time_index(index) is True

    @given(index=invalid_time_indices_low)
    @settings(max_examples=100)
    def test_negative_time_indices_rejected(self, index: int):
        """
        For any negative time_index value, the validation function should
        return False.
        
        **Validates: Requirements 3.3**
        """
        assert validate_time_index(index) is False

    @given(index=invalid_time_indices_high)
    @settings(max_examples=100)
    def test_high_time_indices_rejected(self, index: int):
        """
        For any time_index value greater than 12, the validation function
        should return False.
        
        **Validates: Requirements 3.3**
        """
        assert validate_time_index(index) is False

    @given(index=valid_time_indices)
    @settings(max_examples=100)
    def test_string_time_indices_accepted(self, index: int):
        """
        For any valid time_index as a string, the validation function should
        return True (since it converts to int internally).
        
        **Validates: Requirements 3.3**
        """
        assert validate_time_index(str(index)) is True
