from core.download_engine import DownloadEngine
from core.download_orchestrator import DownloadOrchestrator
from core.download_plugins.registry import DownloadPluginRegistry, PluginSpec


class _FakeClient:
    def __init__(self, configured=True, clear_result=True):
        self.configured = configured
        self.clear_result = clear_result
        self.clear_calls = 0

    def is_configured(self):
        return self.configured

    async def clear_all_completed_downloads(self):
        self.clear_calls += 1
        return self.clear_result


def _build_orchestrator(**clients):
    """Build an orchestrator with mock clients via the registry.

    The orchestrator iterates `self.registry.all_plugins()` to drive
    every per-source operation, so the test must set up a real
    registry with mock plugins (not just stuff attributes on the
    orchestrator). Source slots not provided in `clients` are
    skipped — registry only holds the ones the test cares about.
    """
    registry = DownloadPluginRegistry()
    name_to_display = {
        'soulseek': 'Soulseek', 'youtube': 'YouTube', 'tidal': 'Tidal',
        'qobuz': 'Qobuz', 'hifi': 'HiFi', 'deezer_dl': 'Deezer',
        'lidarr': 'Lidarr', 'soundcloud': 'SoundCloud',
    }
    # 'deezer_dl' is the legacy attr name; canonical registry name is 'deezer'.
    aliases_for = {'deezer_dl': ('deezer_dl',)}
    canonical_for = {'deezer_dl': 'deezer'}

    for slot, client in clients.items():
        if client is None:
            continue
        canonical_name = canonical_for.get(slot, slot)
        registry.register(PluginSpec(
            name=canonical_name,
            factory=lambda c=client: c,
            display_name=name_to_display.get(slot, slot),
            aliases=aliases_for.get(slot, ()),
        ))
    registry.initialize()

    orch = DownloadOrchestrator.__new__(DownloadOrchestrator)
    orch.registry = registry
    orch._init_failures = registry.init_failures
    # Engine — orchestrator delegates per-source query/cancel
    # methods to it, so the test fixture must build one and
    # register every mock plugin under its canonical name.
    orch.engine = DownloadEngine()
    for source_name, plugin in registry.all_plugins():
        orch.engine.register_plugin(source_name, plugin)
    return orch


def _run_async(coro):
    import asyncio

    loop = asyncio.new_event_loop()
    try:
        return loop.run_until_complete(coro)
    finally:
        loop.close()


def test_clear_all_completed_downloads_ignores_unconfigured_clients():
    orch = _build_orchestrator(
        soulseek=_FakeClient(configured=True, clear_result=True),
        youtube=_FakeClient(configured=False, clear_result=False),
    )

    result = _run_async(orch.clear_all_completed_downloads())

    assert result is True
    assert orch.client('soulseek').clear_calls == 1
    assert orch.client('youtube').clear_calls == 0


def test_clear_all_completed_downloads_propagates_configured_failures():
    orch = _build_orchestrator(
        soulseek=_FakeClient(configured=True, clear_result=False),
    )

    result = _run_async(orch.clear_all_completed_downloads())

    assert result is False
    assert orch.client('soulseek').clear_calls == 1


# ---------------------------------------------------------------------------
# Cin-2 generic accessors
# ---------------------------------------------------------------------------


def test_client_returns_registered_client_by_name():
    """Cin's review feedback: orch.client('hifi') is the canonical
    way to reach a per-source client, replacing orch.hifi attribute
    access."""
    soulseek = _FakeClient()
    youtube = _FakeClient()
    orch = _build_orchestrator(soulseek=soulseek, youtube=youtube)

    assert orch.client('soulseek') is soulseek
    assert orch.client('youtube') is youtube
    assert orch.client('made_up') is None


def test_configured_clients_excludes_unconfigured_sources():
    """Replaces the legacy iteration pattern: 6+ if/hasattr/is_configured
    checks per source. Single call returns dict of configured clients."""
    configured = _FakeClient(configured=True)
    unconfigured = _FakeClient(configured=False)
    orch = _build_orchestrator(
        soulseek=configured,
        youtube=unconfigured,
    )
    result = orch.configured_clients()
    assert 'soulseek' in result
    assert 'youtube' not in result
    assert result['soulseek'] is configured


def test_configured_clients_skips_clients_whose_is_configured_raises():
    """Per JohnBaumb: configured_clients() has a try/except so a single
    broken is_configured() call doesn't crash the whole iteration —
    pin it so a future refactor can't quietly drop the guard. The
    broken plugin is skipped; the rest still come back."""

    class _BrokenIsConfigured(_FakeClient):
        def is_configured(self):
            raise RuntimeError("is_configured blew up")

    broken = _BrokenIsConfigured()
    healthy = _FakeClient(configured=True)
    orch = _build_orchestrator(soulseek=healthy, youtube=broken)

    result = orch.configured_clients()
    # Healthy plugin still surfaces; broken one is silently skipped.
    assert 'soulseek' in result
    assert result['soulseek'] is healthy
    assert 'youtube' not in result


def test_reload_instances_dispatches_to_named_source():
    """Generic dispatch — caller passes source name instead of
    reaching for orch.hifi.reload_instances() directly."""

    class _ReloadableClient(_FakeClient):
        def __init__(self):
            super().__init__(configured=True)
            self.reload_called = False

        def reload_instances(self):
            self.reload_called = True

    hifi = _ReloadableClient()
    soulseek = _FakeClient()  # No reload_instances method
    orch = _build_orchestrator(soulseek=soulseek, hifi=hifi)

    assert orch.reload_instances('hifi') is True
    assert hifi.reload_called is True


def test_reload_instances_skips_clients_without_method():
    """Sources that don't expose reload_instances are skipped, not
    treated as failures."""
    soulseek = _FakeClient()  # No reload_instances method
    orch = _build_orchestrator(soulseek=soulseek)
    # Calling on a source without the method = silent no-op
    assert orch.reload_instances('soulseek') is True


def test_reload_instances_with_no_args_reloads_every_source():
    """When called with no source argument, hits every registered
    source that exposes reload_instances."""

    class _ReloadableClient(_FakeClient):
        def __init__(self):
            super().__init__()
            self.reload_called = False

        def reload_instances(self):
            self.reload_called = True

    a = _ReloadableClient()
    b = _ReloadableClient()
    orch = _build_orchestrator(soulseek=a, hifi=b)

    orch.reload_instances()
    assert a.reload_called is True
    assert b.reload_called is True


def test_reload_settings_refreshes_registry_plugins(monkeypatch):
    """Settings saves should refresh plugins that cache config at init.

    Prowlarr-backed torrent / usenet clients keep a ProwlarrClient
    instance, so without this hook newly-saved indexer settings only
    took effect after process restart.
    """

    class _ReloadSettingsClient(_FakeClient):
        def __init__(self):
            super().__init__()
            self.reload_calls = 0

        def reload_settings(self):
            self.reload_calls += 1

    torrent = _ReloadSettingsClient()
    usenet = _ReloadSettingsClient()
    orch = _build_orchestrator(torrent=torrent, usenet=usenet)

    monkeypatch.setattr(
        'core.download_orchestrator.config_manager.get',
        lambda _key, default=None: default,
    )

    orch.reload_settings()

    assert torrent.reload_calls == 1
    assert usenet.reload_calls == 1


# ---------------------------------------------------------------------------
# Singleton factory (matches Cin's get_metadata_engine pattern)
# ---------------------------------------------------------------------------


# ---------------------------------------------------------------------------
# Cin bug 2: hybrid_order alias normalization
# ---------------------------------------------------------------------------


def test_resolve_source_chain_normalizes_legacy_aliases():
    """Cin's bug 2: hybrid_order config containing the legacy alias
    'deezer_dl' was silently dropped because the canonical-name
    membership check rejected it. Orchestrator must normalize via
    the registry alias map first."""
    orch = _build_orchestrator(
        soulseek=_FakeClient(),
        deezer_dl=_FakeClient(),
        youtube=_FakeClient(),
    )
    orch.hybrid_order = ['deezer_dl', 'soulseek', 'youtube']
    orch.hybrid_primary = None
    orch.hybrid_secondary = None

    chain = orch._resolve_source_chain()
    assert chain == ['deezer', 'soulseek', 'youtube']


def test_resolve_source_chain_dedupes_alias_and_canonical():
    """If both 'deezer' and 'deezer_dl' appear, dedupe to single entry."""
    orch = _build_orchestrator(
        soulseek=_FakeClient(),
        deezer_dl=_FakeClient(),
    )
    orch.hybrid_order = ['deezer_dl', 'deezer', 'soulseek']
    orch.hybrid_primary = None
    orch.hybrid_secondary = None

    chain = orch._resolve_source_chain()
    assert chain == ['deezer', 'soulseek']


def test_resolve_source_chain_drops_unknown_names():
    orch = _build_orchestrator(soulseek=_FakeClient(), youtube=_FakeClient())
    orch.hybrid_order = ['nonsense', 'soulseek', 'also_fake', 'youtube']
    orch.hybrid_primary = None
    orch.hybrid_secondary = None

    chain = orch._resolve_source_chain()
    assert chain == ['soulseek', 'youtube']


def test_get_download_orchestrator_returns_set_singleton():
    """When set_download_orchestrator has been called (web_server.py
    does this at boot), get_download_orchestrator returns the
    installed instance instead of building a fresh one."""
    from core.download_orchestrator import (
        get_download_orchestrator,
        set_download_orchestrator,
    )

    orch = _build_orchestrator(soulseek=_FakeClient())
    set_download_orchestrator(orch)
    try:
        assert get_download_orchestrator() is orch
    finally:
        set_download_orchestrator(None)
