"""
Comprehensive tests for news core utilities.
Tests generate_card_id, generate_subscription_id, utc_now, hours_ago, get_local_date_string.
"""

from unittest.mock import Mock, patch
from datetime import datetime, timezone, timedelta
import uuid
import os


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

    def test_returns_string(self):
        """Test 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(self):
        """Test returns a valid UUID string."""
        from local_deep_research.news.core.utils import generate_card_id

        result = generate_card_id()
        # Should be parseable as UUID
        parsed = uuid.UUID(result)
        assert str(parsed) == result

    def test_returns_unique_ids(self):
        """Test returns unique IDs on multiple calls."""
        from local_deep_research.news.core.utils import generate_card_id

        ids = [generate_card_id() for _ in range(100)]
        assert len(set(ids)) == 100

    def test_returns_uuid4_format(self):
        """Test returns UUID version 4."""
        from local_deep_research.news.core.utils import generate_card_id

        result = generate_card_id()
        parsed = uuid.UUID(result)
        assert parsed.version == 4

    def test_returns_lowercase(self):
        """Test returns lowercase UUID."""
        from local_deep_research.news.core.utils import generate_card_id

        result = generate_card_id()
        assert result == result.lower()

    def test_length_is_36(self):
        """Test UUID string length is 36 characters."""
        from local_deep_research.news.core.utils import generate_card_id

        result = generate_card_id()
        assert len(result) == 36


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

    def test_returns_string(self):
        """Test 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(self):
        """Test returns a valid UUID string."""
        from local_deep_research.news.core.utils import generate_subscription_id

        result = generate_subscription_id()
        parsed = uuid.UUID(result)
        assert str(parsed) == result

    def test_returns_unique_ids(self):
        """Test returns unique IDs on multiple calls."""
        from local_deep_research.news.core.utils import generate_subscription_id

        ids = [generate_subscription_id() for _ in range(100)]
        assert len(set(ids)) == 100

    def test_different_from_card_id(self):
        """Test subscription ID is different from card ID (statistically)."""
        from local_deep_research.news.core.utils import (
            generate_card_id,
            generate_subscription_id,
        )

        card_id = generate_card_id()
        sub_id = generate_subscription_id()
        assert card_id != sub_id


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

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

        result = utc_now()
        assert isinstance(result, datetime)

    def test_has_utc_timezone(self):
        """Test returned datetime has UTC timezone."""
        from local_deep_research.news.core.utils import utc_now

        result = utc_now()
        assert result.tzinfo is not None
        assert result.tzinfo == timezone.utc

    def test_is_close_to_current_time(self):
        """Test returned time is close to current time."""
        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_consistent_on_multiple_calls(self):
        """Test times are consistent and increasing."""
        from local_deep_research.news.core.utils import utc_now

        times = [utc_now() for _ in range(10)]

        # Should be monotonically non-decreasing
        for i in range(len(times) - 1):
            assert times[i] <= times[i + 1]

    def test_not_naive_datetime(self):
        """Test returned datetime is not naive."""
        from local_deep_research.news.core.utils import utc_now

        result = utc_now()
        # Naive datetimes have tzinfo=None
        assert result.tzinfo is not None


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

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

        past = utc_now() - timedelta(hours=1)
        result = hours_ago(past)
        assert isinstance(result, float)

    def test_one_hour_ago(self):
        """Test datetime 1 hour ago returns ~1."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        past = utc_now() - timedelta(hours=1)
        result = hours_ago(past)
        assert 0.99 <= result <= 1.01

    def test_two_hours_ago(self):
        """Test datetime 2 hours ago returns ~2."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        past = utc_now() - timedelta(hours=2)
        result = hours_ago(past)
        assert 1.99 <= result <= 2.01

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

        future = utc_now() + timedelta(hours=1)
        result = hours_ago(future)
        assert result < 0
        assert -1.01 <= result <= -0.99

    def test_now_returns_zero(self):
        """Test current datetime returns ~0."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        now = utc_now()
        result = hours_ago(now)
        assert -0.01 <= result <= 0.01

    def test_half_hour_ago(self):
        """Test datetime 30 minutes ago returns ~0.5."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        past = utc_now() - timedelta(minutes=30)
        result = hours_ago(past)
        assert 0.49 <= result <= 0.51

    def test_handles_naive_datetime(self):
        """Test handles naive datetime by assuming UTC."""
        from local_deep_research.news.core.utils import hours_ago

        naive = datetime.utcnow() - timedelta(hours=1)
        result = hours_ago(naive)
        assert 0.99 <= result <= 1.01

    def test_handles_aware_datetime(self):
        """Test handles timezone-aware datetime."""
        from local_deep_research.news.core.utils import hours_ago

        aware = datetime.now(timezone.utc) - timedelta(hours=2)
        result = hours_ago(aware)
        assert 1.99 <= result <= 2.01

    def test_24_hours_ago(self):
        """Test datetime 24 hours ago returns ~24."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        past = utc_now() - timedelta(days=1)
        result = hours_ago(past)
        assert 23.99 <= result <= 24.01

    def test_fractional_hours(self):
        """Test fractional hours are accurate."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        past = utc_now() - timedelta(hours=1, minutes=30)
        result = hours_ago(past)
        assert 1.49 <= result <= 1.51


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

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

        result = get_local_date_string()
        assert isinstance(result, str)

    def test_returns_iso_format(self):
        """Test returns ISO date format (YYYY-MM-DD)."""
        from local_deep_research.news.core.utils import get_local_date_string
        import re

        result = get_local_date_string()
        # Should match YYYY-MM-DD pattern
        assert re.match(r"^\d{4}-\d{2}-\d{2}$", result)

    def test_uses_utc_as_default(self):
        """Test uses UTC as default timezone."""
        from local_deep_research.news.core.utils import get_local_date_string

        # Clear TZ env var
        with patch.dict(os.environ, {}, clear=True):
            result = get_local_date_string()

        expected = datetime.now(timezone.utc).date().isoformat()
        assert result == expected

    def test_uses_settings_manager_timezone(self):
        """Test uses timezone from settings_manager."""
        from local_deep_research.news.core.utils import get_local_date_string
        from zoneinfo import ZoneInfo

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

        result = get_local_date_string(mock_settings)

        mock_settings.get_setting.assert_called_once_with("app.timezone")
        expected = datetime.now(ZoneInfo("America/New_York")).date().isoformat()
        assert result == expected

    def test_uses_tz_env_var_as_fallback(self):
        """Test uses TZ environment variable as fallback."""
        from local_deep_research.news.core.utils import get_local_date_string
        from zoneinfo import ZoneInfo

        with patch.dict(os.environ, {"TZ": "Europe/London"}):
            result = get_local_date_string(None)

        expected = datetime.now(ZoneInfo("Europe/London")).date().isoformat()
        assert result == expected

    def test_handles_invalid_timezone(self):
        """Test handles invalid timezone by falling back to UTC."""
        from local_deep_research.news.core.utils import get_local_date_string

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

        result = get_local_date_string(mock_settings)

        # Should fall back to UTC
        expected = datetime.now(timezone.utc).date().isoformat()
        assert result == expected

    def test_handles_settings_manager_exception(self):
        """Test handles exception from settings_manager."""
        from local_deep_research.news.core.utils import get_local_date_string

        mock_settings = Mock()
        mock_settings.get_setting.side_effect = Exception("Database error")

        with patch.dict(os.environ, {}, clear=True):
            result = get_local_date_string(mock_settings)

        # Should fall back to UTC
        expected = datetime.now(timezone.utc).date().isoformat()
        assert result == expected

    def test_handles_none_settings_manager(self):
        """Test handles None settings_manager."""
        from local_deep_research.news.core.utils import get_local_date_string

        with patch.dict(os.environ, {}, clear=True):
            result = get_local_date_string(None)

        expected = datetime.now(timezone.utc).date().isoformat()
        assert result == expected

    def test_settings_priority_over_env_var(self):
        """Test settings_manager takes priority over TZ env var."""
        from local_deep_research.news.core.utils import get_local_date_string
        from zoneinfo import ZoneInfo

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

        with patch.dict(os.environ, {"TZ": "Europe/Paris"}):
            result = get_local_date_string(mock_settings)

        expected = datetime.now(ZoneInfo("Asia/Tokyo")).date().isoformat()
        assert result == expected

    def test_common_timezones(self):
        """Test works with common timezones."""
        from local_deep_research.news.core.utils import get_local_date_string

        timezones = [
            "UTC",
            "America/New_York",
            "Europe/London",
            "Asia/Tokyo",
            "Australia/Sydney",
            "Pacific/Honolulu",
        ]

        for tz_name in timezones:
            mock_settings = Mock()
            mock_settings.get_setting.return_value = tz_name

            result = get_local_date_string(mock_settings)
            # Should return valid date format
            assert len(result) == 10


class TestUtilsEdgeCases:
    """Edge case tests for utils module."""

    def test_generate_card_id_thread_safe(self):
        """Test generate_card_id is thread-safe."""
        from local_deep_research.news.core.utils import generate_card_id
        import threading

        ids = []
        lock = threading.Lock()

        def generate_ids():
            for _ in range(100):
                card_id = generate_card_id()
                with lock:
                    ids.append(card_id)

        threads = [threading.Thread(target=generate_ids) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # All IDs should be unique
        assert len(ids) == 1000
        assert len(set(ids)) == 1000

    def test_hours_ago_very_old_datetime(self):
        """Test hours_ago with very old datetime."""
        from local_deep_research.news.core.utils import hours_ago

        old = datetime(2020, 1, 1, tzinfo=timezone.utc)
        result = hours_ago(old)
        assert result > 8760  # More than 1 year in hours

    def test_hours_ago_very_far_future(self):
        """Test hours_ago with far future datetime."""
        from local_deep_research.news.core.utils import hours_ago

        future = datetime(2030, 1, 1, tzinfo=timezone.utc)
        result = hours_ago(future)
        assert result < 0

    def test_get_local_date_string_empty_string_timezone(self):
        """Test get_local_date_string with empty string timezone."""
        from local_deep_research.news.core.utils import get_local_date_string

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

        with patch.dict(os.environ, {}, clear=True):
            result = get_local_date_string(mock_settings)

        # Empty string should fall back to UTC
        expected = datetime.now(timezone.utc).date().isoformat()
        assert result == expected

    def test_get_local_date_string_whitespace_timezone(self):
        """Test get_local_date_string with whitespace timezone."""
        from local_deep_research.news.core.utils import get_local_date_string

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

        with patch.dict(os.environ, {}, clear=True):
            result = get_local_date_string(mock_settings)

        # Whitespace should likely be treated as invalid, fall back to UTC
        expected = datetime.now(timezone.utc).date().isoformat()
        assert result == expected

    def test_hours_ago_preserves_precision(self):
        """Test hours_ago preserves sub-hour precision."""
        from local_deep_research.news.core.utils import hours_ago, utc_now

        past = utc_now() - timedelta(hours=3, minutes=45, seconds=30)
        result = hours_ago(past)
        # 3 hours + 45.5 minutes = 3.7583...
        assert 3.75 <= result <= 3.77


class TestUtilsIntegration:
    """Integration tests for utils module."""

    def test_card_id_in_uuid_format_for_database(self):
        """Test card ID can be stored in database UUID field."""
        from local_deep_research.news.core.utils import generate_card_id

        card_id = generate_card_id()
        # Should be valid for PostgreSQL UUID type
        assert len(card_id) == 36
        assert card_id.count("-") == 4

    def test_subscription_id_in_uuid_format_for_database(self):
        """Test subscription ID can be stored in database UUID field."""
        from local_deep_research.news.core.utils import generate_subscription_id

        sub_id = generate_subscription_id()
        assert len(sub_id) == 36
        assert sub_id.count("-") == 4

    def test_utc_now_compatible_with_database_timestamps(self):
        """Test utc_now produces database-compatible timestamps."""
        from local_deep_research.news.core.utils import utc_now

        now = utc_now()
        # Should have standard datetime attributes
        assert hasattr(now, "year")
        assert hasattr(now, "month")
        assert hasattr(now, "day")
        assert hasattr(now, "hour")
        assert hasattr(now, "minute")
        assert hasattr(now, "second")
        assert hasattr(now, "microsecond")

    def test_date_string_format_for_news_placeholders(self):
        """Test date string works as YYYY-MM-DD placeholder replacement."""
        from local_deep_research.news.core.utils import get_local_date_string

        date_str = get_local_date_string()

        # Should work for replacing placeholders
        template = "News from YYYY-MM-DD"
        result = template.replace("YYYY-MM-DD", date_str)
        assert "YYYY-MM-DD" not in result
        assert date_str in result
