"""Pin the structural conformance of every download source plugin
class to ``DownloadSourcePlugin``.

Each registered source class MUST:
- Implement every protocol method by name.
- Mark async methods as `async def` so the orchestrator can `await`
  them uniformly.

When someone adds a new source (e.g. Usenet) and forgets one of
these methods, this test fails at the contract — long before the
first real download attempt would have raised AttributeError in
production. When someone CHANGES the contract (adds a method to
the protocol), this test forces every existing source to be
updated.

Catches the smell that motivated the refactor in the first place:
8 sources independently grew the same shape because every
consumer site needed the same calls, but nothing enforced parity.

NOTE on test design: these tests check CLASSES, not instances.
Instantiating real client classes (TidalDownloadClient, etc.) at
fixture setup pollutes module-level state in tidalapi / spotipy
imports and breaks downstream tests that rely on a clean import
graph. Class-level checks are equally strict for structural
conformance — the protocol only constrains the method surface, not
runtime instance behavior.
"""

from __future__ import annotations

import inspect

import pytest


REQUIRED_SYNC_METHODS = {'is_configured'}

REQUIRED_ASYNC_METHODS = {
    'check_connection',
    'search',
    'download',
    'get_all_downloads',
    'get_download_status',
    'cancel_download',
    'clear_all_completed_downloads',
}


def _import_plugin_classes():
    """Import every download source class lazily inside the test
    rather than at module load — avoids dragging tidalapi /
    spotipy / yt-dlp imports into every other test module's
    collection phase."""
    from core.soulseek_client import SoulseekClient
    from core.youtube_client import YouTubeClient
    from core.tidal_download_client import TidalDownloadClient
    from core.qobuz_client import QobuzClient
    from core.hifi_client import HiFiClient
    from core.deezer_download_client import DeezerDownloadClient
    from core.lidarr_download_client import LidarrDownloadClient
    from core.soundcloud_client import SoundcloudClient
    from core.amazon_download_client import AmazonDownloadClient

    return {
        'soulseek': SoulseekClient,
        'youtube': YouTubeClient,
        'tidal': TidalDownloadClient,
        'qobuz': QobuzClient,
        'hifi': HiFiClient,
        'deezer': DeezerDownloadClient,
        'lidarr': LidarrDownloadClient,
        'soundcloud': SoundcloudClient,
        'amazon': AmazonDownloadClient,
    }


def test_default_registry_registers_all_sources():
    """Smoke check that the foundation registry knows about every
    source the orchestrator historically dispatched to. If someone
    drops a registration here, every other test in this module would
    silently miss the missing source."""
    from core.download_plugins.registry import build_default_registry

    registry = build_default_registry()
    expected = {
        'soulseek', 'youtube', 'tidal', 'qobuz',
        'hifi', 'deezer', 'lidarr', 'soundcloud', 'amazon',
        'torrent', 'usenet',
    }
    assert set(registry.names()) == expected


def test_deezer_dl_alias_is_registered_against_deezer_spec():
    """Legacy ``deezer_dl`` source-name string used in config + per-
    source dispatch must keep resolving — frontend, settings,
    download_orchestrator's username dispatch all depend on it."""
    from core.download_plugins.registry import build_default_registry

    registry = build_default_registry()
    spec = registry.get_spec('deezer_dl')
    assert spec is not None
    assert spec.name == 'deezer'
    assert 'deezer_dl' in spec.aliases


@pytest.mark.parametrize('plugin_name', [
    'soulseek', 'youtube', 'tidal', 'qobuz',
    'hifi', 'deezer', 'lidarr', 'soundcloud', 'amazon',
])
def test_plugin_class_has_all_required_methods(plugin_name):
    """Every registered plugin class exposes every protocol method
    by name. Diagnostic-friendly: tells you WHICH method is missing
    when a new source is added without all the required methods."""
    classes = _import_plugin_classes()
    cls = classes[plugin_name]

    missing = []
    for method_name in REQUIRED_SYNC_METHODS | REQUIRED_ASYNC_METHODS:
        if not hasattr(cls, method_name):
            missing.append(method_name)
    assert not missing, (
        f"{plugin_name} ({cls.__name__}) missing methods: {missing}"
    )


@pytest.mark.parametrize('plugin_name', [
    'soulseek', 'youtube', 'tidal', 'qobuz',
    'hifi', 'deezer', 'lidarr', 'soundcloud', 'amazon',
])
def test_plugin_class_async_methods_are_coroutines(plugin_name):
    """Methods declared async in the protocol must be async on every
    plugin class. A sync `download()` would silently skip the
    orchestrator's `await` and return a coroutine object instead of
    a download_id — the kind of bug that only surfaces at runtime
    against a live user."""
    classes = _import_plugin_classes()
    cls = classes[plugin_name]

    not_async = []
    for method_name in REQUIRED_ASYNC_METHODS:
        method = getattr(cls, method_name, None)
        if method is None:
            continue
        if not inspect.iscoroutinefunction(method):
            not_async.append(method_name)
    assert not not_async, (
        f"{plugin_name} ({cls.__name__}) declared these methods as "
        f"sync but the protocol requires async: {not_async}"
    )


def test_orchestrator_uses_registry_for_dispatch():
    """The orchestrator must hold a registry reference and the generic
    ``client(name)`` accessor must return the same instances the
    registry holds. Per-source attribute aliases (``orchestrator.soulseek``
    etc.) were removed in favor of ``orchestrator.client('soulseek')``;
    the legacy alias name (``deezer_dl``) still resolves to the canonical
    deezer plugin via the registry's alias map."""
    from core.download_orchestrator import DownloadOrchestrator

    orchestrator = DownloadOrchestrator()
    assert hasattr(orchestrator, 'registry')
    assert orchestrator.client('soulseek') is orchestrator.registry.get('soulseek')
    assert orchestrator.client('youtube') is orchestrator.registry.get('youtube')
    assert orchestrator.client('deezer_dl') is orchestrator.registry.get('deezer')
    assert orchestrator.client('lidarr') is orchestrator.registry.get('lidarr')
