"""
Extended tests for news/core/utils.py

Tests cover:
- get_local_date_string() - timezone handling and date formatting
- generate_card_id() - UUID generation
- generate_subscription_id() - UUID generation
- utc_now() - UTC time with timezone
- hours_ago() - time difference calculation
- Edge cases and error handling
"""

import pytest
from unittest.mock import MagicMock
from datetime import datetime, timezone, timedelta
import os


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

    def test_returns_iso_date_format(self):
        """Returns date in YYYY-MM-DD format."""
        from local_deep_research.news.core.utils import get_local_date_string

        result = get_local_date_string()

        # Should match ISO date format
        assert len(result) == 10
        assert result[4] == "-"
        assert result[7] == "-"

    def test_uses_settings_manager_timezone(self):
        """Uses timezone from settings_manager when provided."""
        from local_deep_research.news.core.utils import get_local_date_string

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

        # Clear TZ env var temporarily
        original_tz = os.environ.get("TZ")
        if "TZ" in os.environ:
            del os.environ["TZ"]

        try:
            result = get_local_date_string(settings_manager=mock_settings)
            mock_settings.get_setting.assert_called_once_with("app.timezone")
            assert isinstance(result, str)
        finally:
            if original_tz is not None:
                os.environ["TZ"] = original_tz

    def test_uses_tz_env_var_fallback(self):
        """Uses TZ environment variable when no settings_manager."""
        from local_deep_research.news.core.utils import get_local_date_string

        original_tz = os.environ.get("TZ")

        try:
            os.environ["TZ"] = "Europe/London"
            result = get_local_date_string()
            assert isinstance(result, str)
        finally:
            if original_tz is not None:
                os.environ["TZ"] = original_tz
            elif "TZ" in os.environ:
                del os.environ["TZ"]

    def test_defaults_to_utc(self):
        """Defaults to UTC when no timezone configured."""
        from local_deep_research.news.core.utils import get_local_date_string

        original_tz = os.environ.get("TZ")

        try:
            if "TZ" in os.environ:
                del os.environ["TZ"]

            result = get_local_date_string()
            assert isinstance(result, str)
        finally:
            if original_tz is not None:
                os.environ["TZ"] = original_tz

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

        mock_settings = MagicMock()
        mock_settings.get_setting.side_effect = Exception("DB error")

        # Should not raise, just fall back
        result = get_local_date_string(settings_manager=mock_settings)
        assert isinstance(result, str)

    def test_handles_invalid_timezone(self):
        """Falls back to UTC for 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"

        result = get_local_date_string(settings_manager=mock_settings)
        # Should fall back to UTC and still return valid date
        assert isinstance(result, str)
        assert len(result) == 10

    def test_date_changes_with_timezone(self):
        """Date may differ based on timezone."""
        from local_deep_research.news.core.utils import get_local_date_string

        # This test verifies the function works with different timezones
        # without asserting specific dates (which would be time-dependent)
        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = "Pacific/Auckland"

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

    def test_settings_manager_returns_none(self):
        """Handles settings_manager returning None."""
        from local_deep_research.news.core.utils import get_local_date_string

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

        original_tz = os.environ.get("TZ")
        try:
            if "TZ" in os.environ:
                del os.environ["TZ"]

            result = get_local_date_string(settings_manager=mock_settings)
            assert isinstance(result, str)
        finally:
            if original_tz is not None:
                os.environ["TZ"] = original_tz

    def test_settings_manager_returns_empty_string(self):
        """Handles settings_manager returning empty string."""
        from local_deep_research.news.core.utils import get_local_date_string

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

        original_tz = os.environ.get("TZ")
        try:
            if "TZ" in os.environ:
                del os.environ["TZ"]

            result = get_local_date_string(settings_manager=mock_settings)
            assert isinstance(result, str)
        finally:
            if original_tz is not None:
                os.environ["TZ"] = original_tz


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

    def test_returns_string(self):
        """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_uuid_format(self):
        """Returns string in UUID format (36 characters with dashes)."""
        from local_deep_research.news.core.utils import generate_card_id

        result = generate_card_id()
        assert len(result) == 36
        assert result.count("-") == 4

    def test_generates_unique_ids(self):
        """Generates unique IDs on each call."""
        from local_deep_research.news.core.utils import generate_card_id

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

    def test_id_is_valid_uuid(self):
        """Generated ID is a valid UUID."""
        import uuid
        from local_deep_research.news.core.utils import generate_card_id

        result = generate_card_id()
        # Should not raise
        parsed = uuid.UUID(result)
        assert str(parsed) == result


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

    def test_returns_string(self):
        """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_uuid_format(self):
        """Returns string in UUID format."""
        from local_deep_research.news.core.utils import generate_subscription_id

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

    def test_generates_unique_ids(self):
        """Generates unique IDs on each call."""
        from local_deep_research.news.core.utils import generate_subscription_id

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

    def test_id_is_valid_uuid(self):
        """Generated ID is a valid UUID."""
        import uuid
        from local_deep_research.news.core.utils import generate_subscription_id

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


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

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

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

    def test_has_timezone_info(self):
        """Returned datetime has timezone info."""
        from local_deep_research.news.core.utils import utc_now

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

    def test_is_utc_timezone(self):
        """Returned datetime is in UTC timezone."""
        from local_deep_research.news.core.utils import utc_now

        result = utc_now()
        assert result.tzinfo == timezone.utc

    def test_returns_current_time(self):
        """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_successive_calls_increase(self):
        """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_recent_datetime(self):
        """Calculates hours ago for recent datetime."""
        from local_deep_research.news.core.utils import hours_ago

        # 2 hours ago
        dt = datetime.now(timezone.utc) - timedelta(hours=2)
        result = hours_ago(dt)

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

    def test_exact_hours(self):
        """Calculates exact hours correctly."""
        from local_deep_research.news.core.utils import hours_ago

        # Exactly 24 hours ago
        dt = datetime.now(timezone.utc) - timedelta(hours=24)
        result = hours_ago(dt)

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

    def test_fractional_hours(self):
        """Handles fractional hours."""
        from local_deep_research.news.core.utils import hours_ago

        # 1.5 hours ago (90 minutes)
        dt = datetime.now(timezone.utc) - timedelta(minutes=90)
        result = hours_ago(dt)

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

    def test_future_datetime_negative(self):
        """Future datetime returns negative value."""
        from local_deep_research.news.core.utils import hours_ago

        # 2 hours in the future
        dt = datetime.now(timezone.utc) + timedelta(hours=2)
        result = hours_ago(dt)

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

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

        # Naive datetime (no timezone) - the function adds UTC timezone
        dt = datetime.utcnow() - timedelta(hours=3)
        result = hours_ago(dt)

        # Should be approximately 3 hours (with some tolerance for test execution time)
        assert pytest.approx(result, abs=0.5) == 3.0

    def test_zero_hours(self):
        """Returns approximately zero for current time."""
        from local_deep_research.news.core.utils import hours_ago

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

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

    def test_very_old_datetime(self):
        """Handles very old datetime."""
        from local_deep_research.news.core.utils import hours_ago

        # 30 days ago = 720 hours
        dt = datetime.now(timezone.utc) - timedelta(days=30)
        result = hours_ago(dt)

        assert pytest.approx(result, abs=1) == 720.0

    def test_seconds_precision(self):
        """Handles seconds precision."""
        from local_deep_research.news.core.utils import hours_ago

        # 30 minutes ago = 0.5 hours
        dt = datetime.now(timezone.utc) - timedelta(minutes=30)
        result = hours_ago(dt)

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


class TestEdgeCases:
    """Edge cases for utils functions."""

    def test_generate_card_id_thread_safety(self):
        """IDs are unique even when generated concurrently."""
        from local_deep_research.news.core.utils import generate_card_id
        from concurrent.futures import ThreadPoolExecutor

        with ThreadPoolExecutor(max_workers=10) as executor:
            ids = list(executor.map(lambda _: generate_card_id(), range(100)))

        assert len(ids) == len(set(ids))

    def test_generate_subscription_id_thread_safety(self):
        """Subscription IDs are unique even when generated concurrently."""
        from local_deep_research.news.core.utils import generate_subscription_id
        from concurrent.futures import ThreadPoolExecutor

        with ThreadPoolExecutor(max_workers=10) as executor:
            ids = list(
                executor.map(lambda _: generate_subscription_id(), range(100))
            )

        assert len(ids) == len(set(ids))

    def test_hours_ago_with_different_timezones(self):
        """hours_ago works with different timezone-aware datetimes."""
        from local_deep_research.news.core.utils import hours_ago
        from zoneinfo import ZoneInfo

        # Create datetime in a different timezone
        ny_tz = ZoneInfo("America/New_York")
        dt = datetime.now(ny_tz) - timedelta(hours=5)
        result = hours_ago(dt)

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

    def test_get_local_date_string_all_valid_timezones(self):
        """get_local_date_string works with various valid timezones."""
        from local_deep_research.news.core.utils import get_local_date_string

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

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

            result = get_local_date_string(settings_manager=mock_settings)
            assert isinstance(result, str)
            assert len(result) == 10

    def test_utc_now_called_rapidly(self):
        """utc_now can be called rapidly without issues."""
        from local_deep_research.news.core.utils import utc_now

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

        # All should be valid datetime objects
        assert all(isinstance(t, datetime) for t in times)
        # All should have UTC timezone
        assert all(t.tzinfo == timezone.utc for t in times)
        # Times should be non-decreasing
        for i in range(1, len(times)):
            assert times[i] >= times[i - 1]

    def test_hours_ago_leap_second(self):
        """hours_ago handles times that might span leap seconds."""
        from local_deep_research.news.core.utils import hours_ago

        # Use a fixed datetime that doesn't depend on current time
        dt = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
        # This will give hours from Jan 1, 2024 to now
        result = hours_ago(dt)

        # Should be positive and reasonable
        assert result > 0

    def test_card_id_format_consistent(self):
        """Card ID format is consistent."""
        from local_deep_research.news.core.utils import generate_card_id
        import re

        uuid_pattern = re.compile(
            r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
        )

        for _ in range(10):
            card_id = generate_card_id()
            assert uuid_pattern.match(card_id)

    def test_subscription_id_format_consistent(self):
        """Subscription ID format is consistent."""
        from local_deep_research.news.core.utils import generate_subscription_id
        import re

        uuid_pattern = re.compile(
            r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
        )

        for _ in range(10):
            sub_id = generate_subscription_id()
            assert uuid_pattern.match(sub_id)
