"""
Tests for news/core/utils.py

Tests cover:
- get_local_date_string: timezone priority chain, date boundaries, error handling, DST
- generate_card_id: UUID generation and uniqueness
- generate_subscription_id: UUID generation and uniqueness
- utc_now: timezone-aware UTC datetime
- hours_ago: time difference calculations
"""

import pytest
from datetime import datetime, timezone, timedelta
from unittest.mock import MagicMock, patch
from zoneinfo import ZoneInfo


class TestGetLocalDateString:
    """Tests for get_local_date_string function."""

    def test_uses_settings_manager_timezone_when_provided(self):
        """Test that settings manager timezone has highest priority."""
        from local_deep_research.news.core.utils import get_local_date_string

        # Mock settings manager returning New York timezone
        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = "America/New_York"

        # At 3:00 UTC, it's still 22:00 previous day in NY (-5 hours)
        with patch(
            "local_deep_research.news.core.utils.datetime"
        ) as mock_datetime:
            # Create a real datetime for comparison
            utc_time = datetime(2024, 1, 16, 3, 0, 0, tzinfo=timezone.utc)
            ny_tz = ZoneInfo("America/New_York")

            # Make datetime.now() return our controlled time
            mock_datetime.now.return_value = utc_time.astimezone(ny_tz)
            mock_datetime.side_effect = lambda *args, **kw: datetime(
                *args, **kw
            )

            result = get_local_date_string(settings_manager=mock_settings)

            mock_settings.get_setting.assert_called_once_with("app.timezone")
            # In NY at 3:00 UTC (22:00 prev day EST), date is 2024-01-15
            assert result == "2024-01-15"

    def test_uses_tz_env_when_settings_manager_returns_none(self, monkeypatch):
        """Test TZ environment variable fallback when settings returns None."""
        from local_deep_research.news.core.utils import get_local_date_string

        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = None

        monkeypatch.setenv("TZ", "Asia/Tokyo")

        with patch(
            "local_deep_research.news.core.utils.datetime"
        ) as mock_datetime:
            utc_time = datetime(2024, 1, 15, 20, 0, 0, tzinfo=timezone.utc)
            tokyo_tz = ZoneInfo("Asia/Tokyo")

            mock_datetime.now.return_value = utc_time.astimezone(tokyo_tz)
            mock_datetime.side_effect = lambda *args, **kw: datetime(
                *args, **kw
            )

            result = get_local_date_string(settings_manager=mock_settings)

            # 20:00 UTC = 05:00 next day in Tokyo (+9 hours)
            assert result == "2024-01-16"

    def test_uses_tz_env_when_settings_manager_returns_empty_string(
        self, monkeypatch
    ):
        """Test TZ env fallback when settings returns empty string."""
        from local_deep_research.news.core.utils import get_local_date_string

        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = ""

        monkeypatch.setenv("TZ", "Europe/London")

        with patch(
            "local_deep_research.news.core.utils.datetime"
        ) as mock_datetime:
            utc_time = datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
            london_tz = ZoneInfo("Europe/London")

            mock_datetime.now.return_value = utc_time.astimezone(london_tz)
            mock_datetime.side_effect = lambda *args, **kw: datetime(
                *args, **kw
            )

            result = get_local_date_string(settings_manager=mock_settings)

            # London in summer is UTC+1, so 12:00 UTC = 13:00 BST, same date
            assert result == "2024-06-15"

    def test_uses_tz_env_when_no_settings_manager(self, monkeypatch):
        """Test TZ env fallback when settings_manager is None."""
        from local_deep_research.news.core.utils import get_local_date_string

        monkeypatch.setenv("TZ", "Australia/Sydney")

        with patch(
            "local_deep_research.news.core.utils.datetime"
        ) as mock_datetime:
            utc_time = datetime(2024, 1, 15, 15, 0, 0, tzinfo=timezone.utc)
            sydney_tz = ZoneInfo("Australia/Sydney")

            mock_datetime.now.return_value = utc_time.astimezone(sydney_tz)
            mock_datetime.side_effect = lambda *args, **kw: datetime(
                *args, **kw
            )

            result = get_local_date_string(settings_manager=None)

            # 15:00 UTC = 02:00 next day in Sydney (+11 hours in summer)
            assert result == "2024-01-16"

    def test_defaults_to_utc_when_no_configuration(self, monkeypatch):
        """Test UTC fallback when no timezone configured."""
        from local_deep_research.news.core.utils import get_local_date_string

        # Remove TZ from environment
        monkeypatch.delenv("TZ", raising=False)

        with patch(
            "local_deep_research.news.core.utils.datetime"
        ) as mock_datetime:
            utc_time = datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc)

            mock_datetime.now.return_value = utc_time
            mock_datetime.side_effect = lambda *args, **kw: datetime(
                *args, **kw
            )

            result = get_local_date_string(settings_manager=None)

            assert result == "2024-01-15"

    def test_date_boundary_late_night_positive_offset(self):
        """Test date boundary: 23:30 UTC becomes next day in UTC+9."""
        from local_deep_research.news.core.utils import get_local_date_string

        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = "Asia/Tokyo"

        with patch(
            "local_deep_research.news.core.utils.datetime"
        ) as mock_datetime:
            utc_time = datetime(2024, 1, 15, 23, 30, 0, tzinfo=timezone.utc)
            tokyo_tz = ZoneInfo("Asia/Tokyo")

            mock_datetime.now.return_value = utc_time.astimezone(tokyo_tz)
            mock_datetime.side_effect = lambda *args, **kw: datetime(
                *args, **kw
            )

            result = get_local_date_string(settings_manager=mock_settings)

            # 23:30 UTC = 08:30 next day in Tokyo
            assert result == "2024-01-16"

    def test_date_boundary_early_morning_negative_offset(self):
        """Test date boundary: 02:00 UTC becomes previous day in UTC-8."""
        from local_deep_research.news.core.utils import get_local_date_string

        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = "America/Los_Angeles"

        with patch(
            "local_deep_research.news.core.utils.datetime"
        ) as mock_datetime:
            utc_time = datetime(2024, 1, 16, 2, 0, 0, tzinfo=timezone.utc)
            la_tz = ZoneInfo("America/Los_Angeles")

            mock_datetime.now.return_value = utc_time.astimezone(la_tz)
            mock_datetime.side_effect = lambda *args, **kw: datetime(
                *args, **kw
            )

            result = get_local_date_string(settings_manager=mock_settings)

            # 02:00 UTC = 18:00 previous day in LA (-8 hours)
            assert result == "2024-01-15"

    def test_returns_iso_format_date(self, monkeypatch):
        """Test that result matches YYYY-MM-DD format."""
        from local_deep_research.news.core.utils import get_local_date_string
        import re

        monkeypatch.delenv("TZ", raising=False)

        result = get_local_date_string(settings_manager=None)

        # Validate ISO date format
        assert re.match(r"^\d{4}-\d{2}-\d{2}$", result) is not None

    def test_invalid_timezone_from_settings_falls_back_to_utc(
        self, monkeypatch
    ):
        """Test fallback to UTC when settings has invalid timezone."""
        from local_deep_research.news.core.utils import get_local_date_string

        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = "Invalid/Timezone"

        monkeypatch.delenv("TZ", raising=False)

        # Should not raise, should return UTC date
        result = get_local_date_string(settings_manager=mock_settings)

        # Should return a valid date (UTC)
        assert len(result) == 10
        assert result.count("-") == 2

    def test_invalid_timezone_from_env_falls_back_to_utc(self, monkeypatch):
        """Test fallback to UTC when TZ env has invalid timezone."""
        from local_deep_research.news.core.utils import get_local_date_string

        monkeypatch.setenv("TZ", "NotAReal/Timezone")

        result = get_local_date_string(settings_manager=None)

        # Should return a valid date (UTC)
        assert len(result) == 10

    def test_settings_manager_exception_falls_back_gracefully(
        self, monkeypatch
    ):
        """Test graceful fallback when settings_manager raises exception."""
        from local_deep_research.news.core.utils import get_local_date_string

        mock_settings = MagicMock()
        mock_settings.get_setting.side_effect = RuntimeError(
            "DB connection failed"
        )

        monkeypatch.setenv("TZ", "UTC")

        # Should not crash, should fall back
        result = get_local_date_string(settings_manager=mock_settings)

        assert len(result) == 10

    def test_logs_warning_on_invalid_timezone(self, monkeypatch):
        """Test that warning is logged when timezone is invalid."""
        from local_deep_research.news.core.utils import get_local_date_string

        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = "Invalid/TZ"

        monkeypatch.delenv("TZ", raising=False)

        with patch("local_deep_research.news.core.utils.logger") as mock_logger:
            get_local_date_string(settings_manager=mock_settings)

            # Should log a warning about invalid timezone
            mock_logger.warning.assert_called_once()
            call_args = mock_logger.warning.call_args[0][0]
            assert "Invalid/TZ" in call_args

    def test_handles_dst_spring_forward(self):
        """Test handling of DST spring forward edge case."""
        from local_deep_research.news.core.utils import get_local_date_string

        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = "America/New_York"

        with patch(
            "local_deep_research.news.core.utils.datetime"
        ) as mock_datetime:
            # March 10, 2024 at 2:30 AM - during spring forward
            utc_time = datetime(2024, 3, 10, 7, 30, 0, tzinfo=timezone.utc)
            ny_tz = ZoneInfo("America/New_York")

            mock_datetime.now.return_value = utc_time.astimezone(ny_tz)
            mock_datetime.side_effect = lambda *args, **kw: datetime(
                *args, **kw
            )

            result = get_local_date_string(settings_manager=mock_settings)

            # Should handle gracefully - 7:30 UTC = 3:30 AM EDT
            assert result == "2024-03-10"

    def test_handles_dst_fall_back(self):
        """Test handling of DST fall back edge case."""
        from local_deep_research.news.core.utils import get_local_date_string

        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = "America/New_York"

        with patch(
            "local_deep_research.news.core.utils.datetime"
        ) as mock_datetime:
            # November 3, 2024 at 1:30 AM - during fall back
            utc_time = datetime(2024, 11, 3, 6, 30, 0, tzinfo=timezone.utc)
            ny_tz = ZoneInfo("America/New_York")

            mock_datetime.now.return_value = utc_time.astimezone(ny_tz)
            mock_datetime.side_effect = lambda *args, **kw: datetime(
                *args, **kw
            )

            result = get_local_date_string(settings_manager=mock_settings)

            # Should handle gracefully - 6:30 UTC = 1:30 AM EST
            assert result == "2024-11-03"


class TestGenerateCardId:
    """Tests for generate_card_id function."""

    def test_returns_string(self):
        """Test that generate_card_id returns a string."""
        from local_deep_research.news.core.utils import generate_card_id

        result = generate_card_id()

        assert isinstance(result, str)

    def test_returns_valid_uuid_format(self):
        """Test that result is a valid UUID4 format."""
        from local_deep_research.news.core.utils import generate_card_id
        import uuid

        result = generate_card_id()

        # Should be 36 characters (8-4-4-4-12 with hyphens)
        assert len(result) == 36
        # Should be parseable as UUID
        parsed = uuid.UUID(result)
        # Should be version 4
        assert parsed.version == 4

    def test_returns_unique_values(self):
        """Test that 1000 calls return 1000 unique IDs."""
        from local_deep_research.news.core.utils import generate_card_id

        ids = [generate_card_id() for _ in range(1000)]

        assert len(set(ids)) == 1000

    def test_is_lowercase(self):
        """Test that result is lowercase."""
        from local_deep_research.news.core.utils import generate_card_id

        result = generate_card_id()

        assert result == result.lower()


class TestGenerateSubscriptionId:
    """Tests for generate_subscription_id function."""

    def test_returns_string(self):
        """Test that generate_subscription_id returns a string."""
        from local_deep_research.news.core.utils import generate_subscription_id

        result = generate_subscription_id()

        assert isinstance(result, str)

    def test_returns_valid_uuid_format(self):
        """Test that result is a valid UUID4 format."""
        from local_deep_research.news.core.utils import generate_subscription_id
        import uuid

        result = generate_subscription_id()

        # Should be 36 characters
        assert len(result) == 36
        # Should be parseable as UUID
        parsed = uuid.UUID(result)
        # Should be version 4
        assert parsed.version == 4

    def test_returns_unique_values(self):
        """Test that subscription IDs are unique."""
        from local_deep_research.news.core.utils import generate_subscription_id

        ids = [generate_subscription_id() for _ in range(1000)]

        assert len(set(ids)) == 1000

    def test_different_from_card_ids(self):
        """Test that subscription and card IDs are independent."""
        from local_deep_research.news.core.utils import (
            generate_card_id,
            generate_subscription_id,
        )

        card_ids = set(generate_card_id() for _ in range(100))
        sub_ids = set(generate_subscription_id() for _ in range(100))

        # Should be no overlap (extremely unlikely with UUIDs)
        assert len(card_ids & sub_ids) == 0


class TestUtcNow:
    """Tests for utc_now function."""

    def test_returns_datetime(self):
        """Test that utc_now returns a datetime object."""
        from local_deep_research.news.core.utils import utc_now

        result = utc_now()

        assert isinstance(result, datetime)

    def test_is_timezone_aware(self):
        """Test that returned datetime is timezone-aware."""
        from local_deep_research.news.core.utils import utc_now

        result = utc_now()

        assert result.tzinfo is not None

    def test_timezone_is_utc(self):
        """Test that timezone is UTC."""
        from local_deep_research.news.core.utils import utc_now

        result = utc_now()

        assert result.tzinfo == timezone.utc

    def test_returns_current_time(self):
        """Test that returned time is current (within reasonable tolerance)."""
        from local_deep_research.news.core.utils import utc_now

        before = datetime.now(timezone.utc)
        result = utc_now()
        after = datetime.now(timezone.utc)

        assert before <= result <= after

    def test_successive_calls_increase_monotonically(self):
        """Test that successive calls return increasing times."""
        from local_deep_research.news.core.utils import utc_now
        import time

        t1 = utc_now()
        time.sleep(0.001)  # Small delay
        t2 = utc_now()

        assert t2 >= t1


class TestHoursAgo:
    """Tests for hours_ago function."""

    def test_returns_float(self):
        """Test that hours_ago returns a float."""
        from local_deep_research.news.core.utils import hours_ago

        dt = datetime.now(timezone.utc)
        result = hours_ago(dt)

        assert isinstance(result, float)

    def test_returns_zero_for_current_time(self):
        """Test that current time returns approximately zero."""
        from local_deep_research.news.core.utils import hours_ago

        now = datetime.now(timezone.utc)
        result = hours_ago(now)

        # Should be very close to zero
        assert abs(result) < 0.01

    def test_returns_positive_for_past(self):
        """Test that past datetime returns positive hours."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        two_hours_ago = utc_now() - timedelta(hours=2)
        result = hours_ago(two_hours_ago)

        assert pytest.approx(result, abs=0.01) == 2.0

    def test_returns_negative_for_future(self):
        """Test that future datetime returns negative hours."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        three_hours_ahead = utc_now() + timedelta(hours=3)
        result = hours_ago(three_hours_ahead)

        assert pytest.approx(result, abs=0.01) == -3.0

    def test_handles_naive_datetime_as_utc(self):
        """Test that naive datetime is treated as UTC."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        # Create a naive datetime 1 hour ago in UTC
        one_hour_ago = utc_now() - timedelta(hours=1)
        naive_dt = one_hour_ago.replace(tzinfo=None)

        result = hours_ago(naive_dt)

        assert pytest.approx(result, abs=0.01) == 1.0

    def test_naive_datetime_does_not_modify_original(self):
        """Test that passing naive datetime doesn't modify the original."""
        from local_deep_research.news.core.utils import hours_ago

        naive_dt = datetime(2024, 1, 15, 12, 0, 0)
        original_tzinfo = naive_dt.tzinfo

        hours_ago(naive_dt)

        # Original should still be naive
        assert naive_dt.tzinfo == original_tzinfo

    def test_aware_datetime_preserves_timezone_info(self):
        """Test calculation with non-UTC timezone datetime."""
        from local_deep_research.news.core.utils import hours_ago

        # Create a Tokyo time that's equivalent to a known UTC time
        tokyo_tz = ZoneInfo("Asia/Tokyo")

        with patch(
            "local_deep_research.news.core.utils.utc_now"
        ) as mock_utc_now:
            # Set "now" to 2024-01-15 12:00:00 UTC
            mock_utc_now.return_value = datetime(
                2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc
            )

            # Tokyo is UTC+9, so 21:00 Tokyo = 12:00 UTC
            tokyo_time = datetime(2024, 1, 15, 21, 0, 0, tzinfo=tokyo_tz)

            result = hours_ago(tokyo_time)

            # Should be 0 hours ago since times are equivalent
            assert pytest.approx(result, abs=0.01) == 0.0

    def test_fractional_hours(self):
        """Test that 30 minutes returns 0.5 hours."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        thirty_min_ago = utc_now() - timedelta(minutes=30)
        result = hours_ago(thirty_min_ago)

        assert pytest.approx(result, abs=0.01) == 0.5

    def test_large_time_difference(self):
        """Test calculation with 5 days (120 hours) difference."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        five_days_ago = utc_now() - timedelta(days=5)
        result = hours_ago(five_days_ago)

        assert pytest.approx(result, abs=0.1) == 120.0

    def test_very_small_time_difference(self):
        """Test calculation with 10 seconds difference."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        ten_seconds_ago = utc_now() - timedelta(seconds=10)
        result = hours_ago(ten_seconds_ago)

        expected = 10 / 3600  # 10 seconds in hours
        assert pytest.approx(result, abs=0.001) == expected

    def test_handles_different_timezone_datetime(self):
        """Test correct conversion from different timezone."""
        from local_deep_research.news.core.utils import hours_ago

        ny_tz = ZoneInfo("America/New_York")

        with patch(
            "local_deep_research.news.core.utils.utc_now"
        ) as mock_utc_now:
            # Set "now" to 2024-01-15 17:00:00 UTC
            mock_utc_now.return_value = datetime(
                2024, 1, 15, 17, 0, 0, tzinfo=timezone.utc
            )

            # NY is UTC-5, so 10:00 NY = 15:00 UTC (2 hours before our "now")
            ny_time = datetime(2024, 1, 15, 10, 0, 0, tzinfo=ny_tz)

            result = hours_ago(ny_time)

            # Should be 2 hours ago
            assert pytest.approx(result, abs=0.01) == 2.0
