import datetime
from unittest.mock import patch

import pytest
import pytz

from libs.datetime_utils import naive_utc_now, parse_time_range


def test_naive_utc_now(monkeypatch: pytest.MonkeyPatch):
    tz_aware_utc_now = datetime.datetime.now(tz=datetime.UTC)

    def _now_func(tz: datetime.timezone | None) -> datetime.datetime:
        return tz_aware_utc_now.astimezone(tz)

    monkeypatch.setattr("libs.datetime_utils._now_func", _now_func)

    naive_datetime = naive_utc_now()

    assert naive_datetime.tzinfo is None
    assert naive_datetime.date() == tz_aware_utc_now.date()
    naive_time = naive_datetime.time()
    utc_time = tz_aware_utc_now.time()
    assert naive_time == utc_time


class TestParseTimeRange:
    """Test cases for parse_time_range function."""

    def test_parse_time_range_basic(self):
        """Test basic time range parsing."""
        start, end = parse_time_range("2024-01-01 10:00", "2024-01-01 18:00", "UTC")

        assert start is not None
        assert end is not None
        assert start < end
        assert start.tzinfo == pytz.UTC
        assert end.tzinfo == pytz.UTC

    def test_parse_time_range_start_only(self):
        """Test parsing with only start time."""
        start, end = parse_time_range("2024-01-01 10:00", None, "UTC")

        assert start is not None
        assert end is None
        assert start.tzinfo == pytz.UTC

    def test_parse_time_range_end_only(self):
        """Test parsing with only end time."""
        start, end = parse_time_range(None, "2024-01-01 18:00", "UTC")

        assert start is None
        assert end is not None
        assert end.tzinfo == pytz.UTC

    def test_parse_time_range_both_none(self):
        """Test parsing with both times None."""
        start, end = parse_time_range(None, None, "UTC")

        assert start is None
        assert end is None

    def test_parse_time_range_different_timezones(self):
        """Test parsing with different timezones."""
        # Test with US/Eastern timezone
        start, end = parse_time_range("2024-01-01 10:00", "2024-01-01 18:00", "US/Eastern")

        assert start is not None
        assert end is not None
        assert start.tzinfo == pytz.UTC
        assert end.tzinfo == pytz.UTC
        # Verify the times are correctly converted to UTC
        assert start.hour == 15  # 10 AM EST = 3 PM UTC (in January)
        assert end.hour == 23  # 6 PM EST = 11 PM UTC (in January)

    def test_parse_time_range_invalid_start_format(self):
        """Test parsing with invalid start time format."""
        with pytest.raises(ValueError, match="time data.*does not match format"):
            parse_time_range("invalid-date", "2024-01-01 18:00", "UTC")

    def test_parse_time_range_invalid_end_format(self):
        """Test parsing with invalid end time format."""
        with pytest.raises(ValueError, match="time data.*does not match format"):
            parse_time_range("2024-01-01 10:00", "invalid-date", "UTC")

    def test_parse_time_range_invalid_timezone(self):
        """Test parsing with invalid timezone."""
        with pytest.raises(pytz.exceptions.UnknownTimeZoneError):
            parse_time_range("2024-01-01 10:00", "2024-01-01 18:00", "Invalid/Timezone")

    def test_parse_time_range_start_after_end(self):
        """Test parsing with start time after end time."""
        with pytest.raises(ValueError, match="start must be earlier than or equal to end"):
            parse_time_range("2024-01-01 18:00", "2024-01-01 10:00", "UTC")

    def test_parse_time_range_start_equals_end(self):
        """Test parsing with start time equal to end time."""
        start, end = parse_time_range("2024-01-01 10:00", "2024-01-01 10:00", "UTC")

        assert start is not None
        assert end is not None
        assert start == end

    def test_parse_time_range_dst_ambiguous_time(self):
        """Test parsing during DST ambiguous time (fall back)."""
        # This test simulates DST fall back where 2:30 AM occurs twice
        with patch("pytz.timezone", autospec=True) as mock_timezone:
            # Mock timezone that raises AmbiguousTimeError
            mock_tz = mock_timezone.return_value

            # Create a mock datetime object for the return value
            mock_dt = datetime.datetime(2024, 1, 1, 10, 0, 0)
            mock_utc_dt = mock_dt.replace(tzinfo=pytz.UTC)

            # Create a proper mock for the localized datetime
            from unittest.mock import MagicMock

            mock_localized_dt = MagicMock()
            mock_localized_dt.astimezone.return_value = mock_utc_dt

            # Set up side effects: first call raises exception, second call succeeds
            mock_tz.localize.side_effect = [
                pytz.AmbiguousTimeError("Ambiguous time"),  # First call for start
                mock_localized_dt,  # Second call for start (with is_dst=False)
                pytz.AmbiguousTimeError("Ambiguous time"),  # First call for end
                mock_localized_dt,  # Second call for end (with is_dst=False)
            ]

            start, end = parse_time_range("2024-01-01 10:00", "2024-01-01 18:00", "US/Eastern")

            # Should use is_dst=False for ambiguous times
            assert mock_tz.localize.call_count == 4  # 2 calls per time (first fails, second succeeds)
            assert start is not None
            assert end is not None

    def test_parse_time_range_dst_nonexistent_time(self):
        """Test parsing during DST nonexistent time (spring forward)."""
        with patch("pytz.timezone", autospec=True) as mock_timezone:
            # Mock timezone that raises NonExistentTimeError
            mock_tz = mock_timezone.return_value

            # Create a mock datetime object for the return value
            mock_dt = datetime.datetime(2024, 1, 1, 10, 0, 0)
            mock_utc_dt = mock_dt.replace(tzinfo=pytz.UTC)

            # Create a proper mock for the localized datetime
            from unittest.mock import MagicMock

            mock_localized_dt = MagicMock()
            mock_localized_dt.astimezone.return_value = mock_utc_dt

            # Set up side effects: first call raises exception, second call succeeds
            mock_tz.localize.side_effect = [
                pytz.NonExistentTimeError("Non-existent time"),  # First call for start
                mock_localized_dt,  # Second call for start (with adjusted time)
                pytz.NonExistentTimeError("Non-existent time"),  # First call for end
                mock_localized_dt,  # Second call for end (with adjusted time)
            ]

            start, end = parse_time_range("2024-01-01 10:00", "2024-01-01 18:00", "US/Eastern")

            # Should adjust time forward by 1 hour for nonexistent times
            assert mock_tz.localize.call_count == 4  # 2 calls per time (first fails, second succeeds)
            assert start is not None
            assert end is not None

    def test_parse_time_range_edge_cases(self):
        """Test edge cases for time parsing."""
        # Test with midnight times
        start, end = parse_time_range("2024-01-01 00:00", "2024-01-01 23:59", "UTC")
        assert start is not None
        assert end is not None
        assert start.hour == 0
        assert start.minute == 0
        assert end.hour == 23
        assert end.minute == 59

    def test_parse_time_range_different_dates(self):
        """Test parsing with different dates."""
        start, end = parse_time_range("2024-01-01 10:00", "2024-01-02 10:00", "UTC")
        assert start is not None
        assert end is not None
        assert start.date() != end.date()
        assert (end - start).days == 1

    def test_parse_time_range_seconds_handling(self):
        """Test that seconds are properly set to 0."""
        start, end = parse_time_range("2024-01-01 10:30", "2024-01-01 18:45", "UTC")
        assert start is not None
        assert end is not None
        assert start.second == 0
        assert end.second == 0

    def test_parse_time_range_timezone_conversion_accuracy(self):
        """Test accurate timezone conversion."""
        # Test with a known timezone conversion
        start, end = parse_time_range("2024-01-01 12:00", "2024-01-01 12:00", "Asia/Tokyo")

        assert start is not None
        assert end is not None
        assert start.tzinfo == pytz.UTC
        assert end.tzinfo == pytz.UTC
        # Tokyo is UTC+9, so 12:00 JST = 03:00 UTC
        assert start.hour == 3
        assert end.hour == 3

    def test_parse_time_range_summer_time(self):
        """Test parsing during summer time (DST)."""
        # Test with US/Eastern during summer (EDT = UTC-4)
        start, end = parse_time_range("2024-07-01 12:00", "2024-07-01 12:00", "US/Eastern")

        assert start is not None
        assert end is not None
        assert start.tzinfo == pytz.UTC
        assert end.tzinfo == pytz.UTC
        # 12:00 EDT = 16:00 UTC
        assert start.hour == 16
        assert end.hour == 16

    def test_parse_time_range_winter_time(self):
        """Test parsing during winter time (standard time)."""
        # Test with US/Eastern during winter (EST = UTC-5)
        start, end = parse_time_range("2024-01-01 12:00", "2024-01-01 12:00", "US/Eastern")

        assert start is not None
        assert end is not None
        assert start.tzinfo == pytz.UTC
        assert end.tzinfo == pytz.UTC
        # 12:00 EST = 17:00 UTC
        assert start.hour == 17
        assert end.hour == 17

    def test_parse_time_range_empty_strings(self):
        """Test parsing with empty strings."""
        # Empty strings are treated as None, so they should not raise errors
        start, end = parse_time_range("", "2024-01-01 18:00", "UTC")
        assert start is None
        assert end is not None

        start, end = parse_time_range("2024-01-01 10:00", "", "UTC")
        assert start is not None
        assert end is None

    def test_parse_time_range_malformed_datetime(self):
        """Test parsing with malformed datetime strings."""
        with pytest.raises(ValueError, match="time data.*does not match format"):
            parse_time_range("2024-13-01 10:00", "2024-01-01 18:00", "UTC")

        with pytest.raises(ValueError, match="time data.*does not match format"):
            parse_time_range("2024-01-01 10:00", "2024-01-32 18:00", "UTC")

    def test_parse_time_range_very_long_time_range(self):
        """Test parsing with very long time range."""
        start, end = parse_time_range("2020-01-01 00:00", "2030-12-31 23:59", "UTC")

        assert start is not None
        assert end is not None
        assert start < end
        assert (end - start).days > 3000  # More than 8 years

    def test_parse_time_range_negative_timezone(self):
        """Test parsing with negative timezone offset."""
        start, end = parse_time_range("2024-01-01 12:00", "2024-01-01 12:00", "America/New_York")

        assert start is not None
        assert end is not None
        assert start.tzinfo == pytz.UTC
        assert end.tzinfo == pytz.UTC
