"""Tests for sync coordinator: cooldown on success only, future dedup, race safety."""
import asyncio
import time

import pytest
from unittest.mock import AsyncMock, MagicMock, patch


def _make_library_service(**overrides):
    """Build a minimal LibraryService with mocked deps."""
    from services.library_service import LibraryService

    lidarr = overrides.get("lidarr", AsyncMock())
    lidarr.is_configured = MagicMock(return_value=True)
    lidarr.get_library = overrides.get("get_library", AsyncMock(return_value=[]))
    lidarr.get_artists_from_library = overrides.get(
        "get_artists_from_library", AsyncMock(return_value=[])
    )

    library_db = overrides.get("library_db", AsyncMock())
    library_db.save_library = AsyncMock()

    prefs = overrides.get("prefs", MagicMock())
    prefs.get_advanced_settings.return_value = MagicMock(
        cache_ttl_library_sync=30,
    )

    svc = LibraryService(
        lidarr_repo=lidarr,
        library_db=library_db,
        cover_repo=MagicMock(),
        preferences_service=prefs,
    )
    return svc


class TestCooldownOnlyOnSuccess:
    @pytest.mark.asyncio
    async def test_failed_sync_does_not_set_cooldown(self):
        svc = _make_library_service()
        svc._lidarr_repo.get_library = AsyncMock(side_effect=RuntimeError("DNS fail"))

        with patch("services.library_service.CacheStatusService") as mock_css:
            mock_css.return_value.is_syncing.return_value = False

            with pytest.raises(Exception, match="DNS fail"):
                await svc.sync_library()

        assert svc._last_sync_time == 0.0, "cooldown must NOT activate on failure"

    @pytest.mark.asyncio
    async def test_successful_sync_sets_cooldown(self):
        svc = _make_library_service()

        with patch("services.library_service.CacheStatusService") as mock_css:
            mock_css.return_value.is_syncing.return_value = False

            before = time.time()
            result = await svc.sync_library()
            after = time.time()

        assert result.status == "success"
        assert before <= svc._last_sync_time <= after

    @pytest.mark.asyncio
    async def test_retry_after_failed_sync_is_not_cooldown_blocked(self):
        call_count = 0

        async def fail_then_succeed():
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise RuntimeError("temporary failure")
            return []

        svc = _make_library_service()
        svc._lidarr_repo.get_library = fail_then_succeed

        with patch("services.library_service.CacheStatusService") as mock_css:
            mock_css.return_value.is_syncing.return_value = False

            with pytest.raises(Exception, match="temporary failure"):
                await svc.sync_library()

            result = await svc.sync_library()

        assert result.status == "success"


class TestSyncFutureDedup:
    @pytest.mark.asyncio
    async def test_concurrent_syncs_deduplicated(self):
        """Two concurrent sync calls should result in exactly one Lidarr call."""
        call_count = 0
        sync_event = asyncio.Event()

        async def slow_get_library():
            nonlocal call_count
            call_count += 1
            sync_event.set()
            await asyncio.sleep(0.05)
            return []

        svc = _make_library_service()
        svc._lidarr_repo.get_library = slow_get_library

        with patch("services.library_service.CacheStatusService") as mock_css:
            mock_css.return_value.is_syncing.return_value = False

            results = await asyncio.gather(
                svc.sync_library(),
                svc.sync_library(),
            )

        assert all(r.status == "success" for r in results)
        assert call_count == 1, f"Expected 1 Lidarr call, got {call_count}"

    @pytest.mark.asyncio
    async def test_concurrent_sync_failure_propagates_to_waiter(self):
        """When the producer fails, deduped waiters get the real exception."""
        async def failing_get_library():
            await asyncio.sleep(0.05)
            raise RuntimeError("Lidarr DNS failure")

        svc = _make_library_service()
        svc._lidarr_repo.get_library = failing_get_library

        with patch("services.library_service.CacheStatusService") as mock_css:
            mock_css.return_value.is_syncing.return_value = False

            results = await asyncio.gather(
                svc.sync_library(),
                svc.sync_library(),
                return_exceptions=True,
            )

        # Both should get an error, not hang or get CancelledError
        for r in results:
            assert isinstance(r, Exception)
            assert not isinstance(r, asyncio.CancelledError), \
                "Waiter got CancelledError instead of the real exception"
