#!/usr/bin/env python3
"""
Comprehensive boundary condition tests for input validation.

This module tests edge cases at the limits of valid input ranges for:
- Date boundaries (1900-01-01 to 2100-12-31)
- Time index boundaries (0 to 12)
- Lunar leap month validation

Requirements: 3.1, 3.2, 3.3, 3.4
"""

import pytest

from core.birth_info import BirthInfo
from core.exceptions import DateRangeError, ValidationError
from utils.validators import (
    validate_date,
    validate_date_range,
    validate_time_index,
)


class TestDateBoundaries:
    """
    Date boundary tests for the supported range (1900-2100).
    
    Requirements: 3.1, 3.2
    """

    def test_minimum_valid_date(self):
        """Test that 1900-01-01 (minimum boundary) is accepted."""
        # Should not raise any exception
        validate_date_range("1900-01-01")
        assert validate_date("1900-01-01") is True

    def test_maximum_valid_date(self):
        """Test that 2100-12-31 (maximum boundary) is accepted."""
        # Should not raise any exception
        validate_date_range("2100-12-31")
        assert validate_date("2100-12-31") is True

    def test_date_just_before_minimum(self):
        """Test that 1899-12-31 (one day before minimum) is rejected."""
        with pytest.raises(DateRangeError) as exc_info:
            validate_date_range("1899-12-31")
        error_msg = str(exc_info.value)
        assert "1899-12-31" in error_msg  # Should include the invalid value
        assert "1900" in error_msg and "2100" in error_msg  # Should include valid range

    def test_date_just_after_maximum(self):
        """Test that 2101-01-01 (one day after maximum) is rejected."""
        with pytest.raises(DateRangeError) as exc_info:
            validate_date_range("2101-01-01")
        error_msg = str(exc_info.value)
        assert "2101-01-01" in error_msg  # Should include the invalid value
        assert "1900" in error_msg and "2100" in error_msg  # Should include valid range

    def test_year_1899_rejected(self):
        """Test that any date in year 1899 is rejected."""
        with pytest.raises(DateRangeError):
            validate_date_range("1899-06-15")

    def test_year_2101_rejected(self):
        """Test that any date in year 2101 is rejected."""
        with pytest.raises(DateRangeError):
            validate_date_range("2101-06-15")

    def test_first_day_of_valid_range(self):
        """Test first day of each boundary year."""
        validate_date_range("1900-01-01")
        validate_date_range("2100-01-01")

    def test_last_day_of_valid_range(self):
        """Test last day of each boundary year."""
        validate_date_range("1900-12-31")
        validate_date_range("2100-12-31")

    def test_leap_year_february_29_valid(self):
        """Test February 29 in leap years is accepted (Requirements: 3.5)."""
        # 2000 is a leap year (divisible by 400)
        assert validate_date("2000-02-29") is True
        # 2004 is a leap year (divisible by 4, not century)
        assert validate_date("2004-02-29") is True
        # 2020 is a leap year
        assert validate_date("2020-02-29") is True

    def test_non_leap_year_february_29_invalid(self):
        """Test February 29 in non-leap years is rejected (Requirements: 3.6)."""
        # 2001 is not a leap year
        assert validate_date("2001-02-29") is False
        # 1900 is not a leap year (century not divisible by 400)
        assert validate_date("1900-02-29") is False
        # 2100 is not a leap year (century not divisible by 400)
        assert validate_date("2100-02-29") is False

    def test_century_leap_year_rules(self):
        """Test century leap year rules (divisible by 400)."""
        # 2000 is a leap year (divisible by 400)
        assert validate_date("2000-02-29") is True
        # 1900 is NOT a leap year (divisible by 100 but not 400)
        assert validate_date("1900-02-29") is False


class TestTimeIndexBoundaries:
    """
    Time index boundary tests for valid range (0-12).
    
    Requirements: 3.3
    """

    def test_early_zi_hour_valid(self):
        """Test time_index 0 (early Zi hour, 23:00-01:00) is valid."""
        assert validate_time_index(0) is True

    def test_late_zi_hour_valid(self):
        """Test time_index 12 (late Zi hour, 23:00-01:00) is valid."""
        assert validate_time_index(12) is True

    def test_negative_time_index_invalid(self):
        """Test time_index -1 (below minimum) is invalid."""
        assert validate_time_index(-1) is False

    def test_time_index_above_maximum_invalid(self):
        """Test time_index 13 (above maximum) is invalid."""
        assert validate_time_index(13) is False

    def test_all_valid_time_indices(self):
        """Test all valid time indices (0-12) are accepted."""
        for i in range(13):
            assert validate_time_index(i) is True, f"time_index {i} should be valid"

    def test_large_invalid_time_index(self):
        """Test very large time_index values are rejected."""
        assert validate_time_index(100) is False
        assert validate_time_index(1000) is False

    def test_negative_large_time_index(self):
        """Test very negative time_index values are rejected."""
        assert validate_time_index(-100) is False
        assert validate_time_index(-1000) is False

    def test_string_time_index_conversion(self):
        """Test string time_index values are properly converted."""
        assert validate_time_index("0") is True
        assert validate_time_index("12") is True
        assert validate_time_index("-1") is False
        assert validate_time_index("13") is False

    def test_invalid_string_time_index(self):
        """Test non-numeric string time_index values are rejected."""
        assert validate_time_index("invalid") is False
        assert validate_time_index("") is False
        assert validate_time_index("abc") is False

    def test_none_time_index(self):
        """Test None time_index is rejected."""
        assert validate_time_index(None) is False

    def test_float_time_index(self):
        """Test float time_index values."""
        # Float that converts to valid int
        assert validate_time_index(6.0) is True
        assert validate_time_index(0.0) is True
        assert validate_time_index(12.0) is True


class TestLunarLeapMonthBoundaries:
    """
    Lunar leap month validation tests.
    
    Requirements: 3.4
    """

    def test_valid_lunar_date_without_leap_month(self):
        """Test valid lunar date without leap month flag."""
        birth_info = BirthInfo(
            date="2000-08-16",
            time_index=6,
            gender="女",
            calendar="lunar",
            is_leap_month=False,
        )
        assert birth_info.calendar == "lunar"
        assert birth_info.is_leap_month is False

    def test_valid_lunar_date_with_leap_month(self):
        """Test valid lunar date with leap month flag."""
        birth_info = BirthInfo(
            date="2000-08-16",
            time_index=6,
            gender="女",
            calendar="lunar",
            is_leap_month=True,
        )
        assert birth_info.calendar == "lunar"
        assert birth_info.is_leap_month is True

    def test_solar_date_ignores_leap_month(self):
        """Test that solar calendar ignores is_leap_month flag."""
        # is_leap_month should be ignored for solar calendar
        birth_info = BirthInfo(
            date="2000-08-16",
            time_index=6,
            gender="女",
            calendar="solar",
            is_leap_month=True,  # This should be ignored for solar
        )
        assert birth_info.calendar == "solar"
        # The flag is stored but should be ignored in calculations

    def test_leap_month_default_false(self):
        """Test that is_leap_month defaults to False."""
        birth_info = BirthInfo(
            date="2000-08-16",
            time_index=6,
            gender="女",
        )
        assert birth_info.is_leap_month is False

    def test_lunar_calendar_with_various_time_indices(self):
        """Test lunar calendar works with all valid time indices."""
        for time_index in range(13):
            birth_info = BirthInfo(
                date="2000-08-16",
                time_index=time_index,
                gender="男",
                calendar="lunar",
                is_leap_month=False,
            )
            assert birth_info.time_index == time_index

    def test_invalid_calendar_type(self):
        """Test that invalid calendar type raises error."""
        with pytest.raises(ValueError) as exc_info:
            BirthInfo(
                date="2000-08-16",
                time_index=6,
                gender="女",
                calendar="gregorian",  # Invalid calendar type
            )
        assert "solar" in str(exc_info.value) or "lunar" in str(exc_info.value)


class TestBirthInfoBoundaries:
    """
    BirthInfo boundary tests combining multiple validations.
    
    Requirements: 3.1, 3.3, 3.4
    """

    def test_birth_info_with_minimum_date(self):
        """Test BirthInfo creation with minimum valid date."""
        birth_info = BirthInfo(
            date="1900-01-01",
            time_index=0,
            gender="男",
        )
        assert birth_info.date == "1900-01-01"

    def test_birth_info_with_maximum_date(self):
        """Test BirthInfo creation with maximum valid date."""
        birth_info = BirthInfo(
            date="2100-12-31",
            time_index=12,
            gender="女",
        )
        assert birth_info.date == "2100-12-31"

    def test_birth_info_with_boundary_time_indices(self):
        """Test BirthInfo with boundary time indices."""
        # Early Zi hour
        birth_info_early = BirthInfo(
            date="2000-08-16",
            time_index=0,
            gender="男",
        )
        assert birth_info_early.time_index == 0
        assert birth_info_early.get_time_name() == "早子时"

        # Late Zi hour
        birth_info_late = BirthInfo(
            date="2000-08-16",
            time_index=12,
            gender="女",
        )
        assert birth_info_late.time_index == 12
        assert birth_info_late.get_time_name() == "晚子时"

    def test_birth_info_invalid_time_index_below(self):
        """Test BirthInfo rejects time_index below 0."""
        with pytest.raises(ValueError) as exc_info:
            BirthInfo(
                date="2000-08-16",
                time_index=-1,
                gender="男",
            )
        assert "0-12" in str(exc_info.value)

    def test_birth_info_invalid_time_index_above(self):
        """Test BirthInfo rejects time_index above 12."""
        with pytest.raises(ValueError) as exc_info:
            BirthInfo(
                date="2000-08-16",
                time_index=13,
                gender="女",
            )
        assert "0-12" in str(exc_info.value)

    def test_birth_info_invalid_gender(self):
        """Test BirthInfo rejects invalid gender."""
        with pytest.raises(ValueError):
            BirthInfo(
                date="2000-08-16",
                time_index=6,
                gender="male",  # Invalid - should be "男" or "女"
            )

    def test_birth_info_invalid_date_format(self):
        """Test BirthInfo rejects invalid date format."""
        with pytest.raises(ValueError) as exc_info:
            BirthInfo(
                date="2000/08/16",  # Wrong format
                time_index=6,
                gender="男",
            )
        assert "YYYY-MM-DD" in str(exc_info.value)

    def test_birth_info_time_range_boundaries(self):
        """Test time range strings for boundary time indices."""
        birth_info_0 = BirthInfo(date="2000-08-16", time_index=0, gender="男")
        birth_info_12 = BirthInfo(date="2000-08-16", time_index=12, gender="女")
        
        # Both early and late Zi hour should have same time range
        assert birth_info_0.get_time_range() == "23:00~01:00"
        assert birth_info_12.get_time_range() == "23:00~01:00"
