"""Integration tests for SoundCloud wiring inside DownloadOrchestrator.

The standalone SoundcloudClient is exhaustively unit-tested in
``tests/test_soundcloud_client.py``. These tests verify the *plumbing*:
the orchestrator constructs a SoundCloud client at startup, exposes it
via the same lookup APIs every other source uses, dispatches downloads
to it when the username matches, and includes it in the hybrid-mode
fan-out / status / cancel / clear paths.

The intent is plug-and-play parity: any code that walks the
orchestrator's source list (UI, status endpoints, batch tracker)
picks up SoundCloud automatically without per-source special cases.
"""

from __future__ import annotations

import asyncio
from unittest.mock import patch, MagicMock

import pytest

from core.download_orchestrator import DownloadOrchestrator
from core.soundcloud_client import SoundcloudClient


def _run(coro):
    return asyncio.run(coro)


@pytest.fixture
def orchestrator() -> DownloadOrchestrator:
    """Real orchestrator with real (but mostly idle) clients."""
    return DownloadOrchestrator()


# ---------------------------------------------------------------------------
# Construction
# ---------------------------------------------------------------------------


def test_orchestrator_constructs_soundcloud_client(orchestrator: DownloadOrchestrator) -> None:
    assert orchestrator.client('soundcloud') is not None
    assert isinstance(orchestrator.client('soundcloud'), SoundcloudClient)


def test_client_lookup_resolves_soundcloud(orchestrator: DownloadOrchestrator) -> None:
    """Verify the registry-backed name → client lookup includes SoundCloud."""
    assert orchestrator.client('soundcloud') is orchestrator.registry.get('soundcloud')


def test_client_lookup_returns_none_for_unknown(orchestrator: DownloadOrchestrator) -> None:
    """Sanity: unknown sources don't somehow resolve to SoundCloud."""
    assert orchestrator.client('made_up') is None


# ---------------------------------------------------------------------------
# Status surface
# ---------------------------------------------------------------------------


def test_get_source_status_includes_soundcloud(orchestrator: DownloadOrchestrator) -> None:
    """Every other source has a key here; SoundCloud should too. The UI
    walks this dict to render configured-status badges."""
    status = orchestrator.get_source_status()
    assert 'soundcloud' in status
    # yt-dlp is in requirements.txt → SoundCloud is configured by default
    assert status['soundcloud'] is True


# ---------------------------------------------------------------------------
# Download dispatch
# ---------------------------------------------------------------------------


def test_download_routes_soundcloud_username_to_client(orchestrator: DownloadOrchestrator) -> None:
    """The dispatcher must route ``username='soundcloud'`` to the SoundCloud
    client, not to Soulseek (the default fallback path)."""
    sentinel = 'sc-download-id-xyz'

    async def _fake_download(username, filename, file_size=0):
        return sentinel

    with patch.object(orchestrator.client('soundcloud'), 'download', side_effect=_fake_download) as mock_dl:
        result = _run(orchestrator.download(
            'soundcloud',
            '999||https://soundcloud.com/x/y||Display',
            file_size=0,
        ))
    assert result == sentinel
    mock_dl.assert_called_once()


def test_download_unknown_username_still_falls_to_soulseek(orchestrator: DownloadOrchestrator) -> None:
    """Adding SoundCloud must not change the legacy Soulseek-fallback
    behavior for unrecognized usernames."""
    if orchestrator.client('soulseek') is None:
        pytest.skip("Soulseek client unavailable in this environment")

    async def _fake_soulseek_download(username, filename, file_size=0):
        return 'soulseek-id'

    with patch.object(orchestrator.client('soulseek'), 'download', side_effect=_fake_soulseek_download) as mock_dl:
        result = _run(orchestrator.download('some_random_user', 'file.mp3', 0))
    assert result == 'soulseek-id'
    mock_dl.assert_called_once()


# ---------------------------------------------------------------------------
# Hybrid mode
# ---------------------------------------------------------------------------


def test_hybrid_search_iterates_soundcloud_when_in_order(orchestrator: DownloadOrchestrator) -> None:
    """When SoundCloud appears in the hybrid_order list, the orchestrator
    must walk through its search results just like any other source."""
    orchestrator.mode = 'hybrid'
    orchestrator.hybrid_order = ['soundcloud']

    fake_track = MagicMock()
    fake_track.username = 'soundcloud'

    async def _fake_search(query, timeout=None, progress_callback=None):
        return ([fake_track], [])

    with patch.object(orchestrator.client('soundcloud'), 'search', side_effect=_fake_search), \
         patch.object(orchestrator.client('soundcloud'), 'is_configured', return_value=True):
        tracks, albums = _run(orchestrator.search("any query"))

    assert tracks == [fake_track]
    assert albums == []


def test_hybrid_search_skips_unconfigured_soundcloud(orchestrator: DownloadOrchestrator) -> None:
    """Defensive: if SoundCloud is unconfigured (yt-dlp missing), the
    hybrid loop must skip it cleanly and continue to the next source."""
    orchestrator.mode = 'hybrid'
    orchestrator.hybrid_order = ['soundcloud', 'soulseek']

    if orchestrator.client('soulseek') is None:
        pytest.skip("Soulseek client unavailable in this environment")

    soulseek_track = MagicMock()
    soulseek_track.username = 'unrelated_user'

    async def _fake_soulseek_search(query, timeout=None, progress_callback=None):
        return ([soulseek_track], [])

    with patch.object(orchestrator.client('soundcloud'), 'is_configured', return_value=False), \
         patch.object(orchestrator.client('soulseek'), 'is_configured', return_value=True), \
         patch.object(orchestrator.client('soulseek'), 'search', side_effect=_fake_soulseek_search):
        tracks, _ = _run(orchestrator.search("any"))

    assert tracks == [soulseek_track]


# ---------------------------------------------------------------------------
# Aggregate operations
# ---------------------------------------------------------------------------


def test_get_all_downloads_walks_soundcloud(orchestrator: DownloadOrchestrator) -> None:
    """Active-downloads endpoint pulls from every client; SoundCloud's
    queue must show up in the aggregate."""
    fake_status = MagicMock(id='sc-1', filename='x', state='InProgress, Downloading')

    async def _fake_get_all():
        return [fake_status]

    with patch.object(orchestrator.client('soundcloud'), 'get_all_downloads', side_effect=_fake_get_all):
        all_dl = _run(orchestrator.get_all_downloads())

    assert any(d is fake_status for d in all_dl)


def test_get_download_status_finds_soundcloud_id(orchestrator: DownloadOrchestrator) -> None:
    """Status lookup must check SoundCloud — orchestrator iterates every
    client until one finds the id."""
    fake_status = MagicMock(id='sc-2')

    async def _fake_get_status(download_id):
        return fake_status if download_id == 'sc-2' else None

    with patch.object(orchestrator.client('soundcloud'), 'get_download_status', side_effect=_fake_get_status):
        result = _run(orchestrator.get_download_status('sc-2'))

    assert result is fake_status


def test_cancel_routes_soundcloud_username(orchestrator: DownloadOrchestrator) -> None:
    """Username-routed cancel must dispatch to the SoundCloud client when
    username='soundcloud' is provided."""
    async def _fake_cancel(download_id, username=None, remove=False):
        return True

    with patch.object(orchestrator.client('soundcloud'), 'cancel_download', side_effect=_fake_cancel) as mock_cancel:
        ok = _run(orchestrator.cancel_download('sc-3', username='soundcloud'))
    assert ok is True
    mock_cancel.assert_called_once()


def test_clear_completed_walks_soundcloud(orchestrator: DownloadOrchestrator) -> None:
    """Bulk clear-all-completed must call SoundCloud's clear method.

    We assert SoundCloud got called — not that the overall result is
    True, since other sibling clients in the same orchestrator may
    return False for unrelated reasons (e.g. an unrelated client
    throwing). The contract this test pins is "SoundCloud is included
    in the iteration", which is what plug-and-play parity requires.
    """
    async def _fake_clear():
        return True

    with patch.object(orchestrator.client('soundcloud'), 'clear_all_completed_downloads', side_effect=_fake_clear) as mock_clear:
        _run(orchestrator.clear_all_completed_downloads())
    mock_clear.assert_called_once()


# ---------------------------------------------------------------------------
# Mode-only routing
# ---------------------------------------------------------------------------


def test_soundcloud_only_mode_uses_soundcloud(orchestrator: DownloadOrchestrator) -> None:
    """When mode='soundcloud', search must be dispatched only to the
    SoundCloud client — not soulseek or any other source."""
    orchestrator.mode = 'soundcloud'

    async def _fake_search(query, timeout=None, progress_callback=None):
        return ([MagicMock(username='soundcloud')], [])

    with patch.object(orchestrator.client('soundcloud'), 'search', side_effect=_fake_search) as mock_sc, \
         patch.object(orchestrator.client('soulseek'), 'search', side_effect=AssertionError("soulseek must not be searched")):
        tracks, _ = _run(orchestrator.search("any"))

    assert len(tracks) == 1
    mock_sc.assert_called_once()


def test_streaming_sources_tuple_includes_soundcloud() -> None:
    """The validation/streaming-source tuples used to pick scoring
    behavior must include SoundCloud — otherwise SoundCloud results
    would skip the matching-engine validation in
    search_and_download_best."""
    from core.downloads import validation
    from inspect import getsource
    src = getsource(validation.filter_streaming_results) if hasattr(validation, 'filter_streaming_results') else getsource(validation)
    assert 'soundcloud' in src, "core.downloads.validation must include 'soundcloud' in _streaming_sources"
