import sys
import types
from datetime import datetime, timedelta

_RECENT_RELEASE_DATE = (datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")

if "spotipy" not in sys.modules:
    spotipy = types.ModuleType("spotipy")

    class _DummySpotify:
        def __init__(self, *args, **kwargs):
            pass

    oauth2 = types.ModuleType("spotipy.oauth2")

    class _DummyOAuth:
        def __init__(self, *args, **kwargs):
            pass

    spotipy.Spotify = _DummySpotify
    oauth2.SpotifyOAuth = _DummyOAuth
    oauth2.SpotifyClientCredentials = _DummyOAuth
    spotipy.oauth2 = oauth2
    sys.modules["spotipy"] = spotipy
    sys.modules["spotipy.oauth2"] = oauth2

if "config.settings" not in sys.modules:
    config_pkg = types.ModuleType("config")
    settings_mod = types.ModuleType("config.settings")

    class _DummyConfigManager:
        def get(self, key, default=None):
            return default

        def get_active_media_server(self):
            return "plex"

    settings_mod.config_manager = _DummyConfigManager()
    config_pkg.settings = settings_mod
    sys.modules["config"] = config_pkg
    sys.modules["config.settings"] = settings_mod

if "core.matching_engine" not in sys.modules:
    matching_engine_mod = types.ModuleType("core.matching_engine")

    class _DummyMatchingEngine:
        def clean_title(self, title):
            return title

    matching_engine_mod.MusicMatchingEngine = _DummyMatchingEngine
    sys.modules["core.matching_engine"] = matching_engine_mod

import core.watchlist_scanner as watchlist_scanner_module
from core.watchlist_scanner import WatchlistScanner


class _FakeSpotifyClient:
    def __init__(self, search_results=None):
        self.search_results = list(search_results or [])
        self.search_calls = []

    def is_spotify_authenticated(self):
        return False

    def search_artists(self, query, limit=1, allow_fallback=True):
        self.search_calls.append((query, limit, allow_fallback))
        return list(self.search_results) if allow_fallback else []


class _FakeMetadataService:
    def __init__(self, album_data, spotify_client=None):
        self.spotify = spotify_client or _FakeSpotifyClient()
        self.itunes = types.SimpleNamespace()
        self._album_data = album_data

    def get_album(self, album_id):
        return self._album_data


class _FakeSourceClient:
    def __init__(self, *, artist_id: str, albums, image_url: str, album_payload=None, album_search_results=None):
        self.artist_id = artist_id
        self.albums = list(albums)
        self.image_url = image_url
        self.album_payload = album_payload
        self.album_search_results = list(album_search_results or [])
        self.search_calls = []
        self.search_album_calls = []
        self.album_calls = []
        self.artist_calls = []

    def search_artists(self, query, limit=1, **kwargs):
        self.search_calls.append((query, limit, kwargs))
        return [types.SimpleNamespace(id=self.artist_id, name=query)]

    def search_albums(self, query, limit=1, **kwargs):
        self.search_album_calls.append((query, limit, kwargs))
        return list(self.album_search_results)

    def get_artist_albums(self, artist_id, album_type='album,single', limit=50, **kwargs):
        self.album_calls.append((artist_id, album_type, limit, kwargs))
        return list(self.albums)

    def get_artist(self, artist_id, **kwargs):
        self.artist_calls.append(artist_id)
        return {
            "id": artist_id,
            "images": [{"url": self.image_url}] if self.image_url else [],
        }

    def get_album(self, album_id, **kwargs):
        self.album_calls.append((album_id, kwargs))
        if self.album_payload is not None:
            return self.album_payload
        return {
            "id": album_id,
            "name": "Album One",
            "images": [{"url": self.image_url}] if self.image_url else [],
            "tracks": {"items": []},
            "artists": [{"id": self.artist_id}],
        }


class _FakeDB:
    def __init__(self, artists):
        self.artists = artists
        self.similar_calls = []
        self.discovery_pool_calls = []
        self.discovery_pool_timestamp_calls = []
        self.discovery_recent_calls = []
        self.db_albums = []

    def get_watchlist_artists(self, profile_id=None):
        return list(self.artists)

    def has_fresh_similar_artists(self, *args, **kwargs):
        self.similar_calls.append((args, kwargs))
        return False

    def should_populate_discovery_pool(self, hours_threshold=24, profile_id=1):
        return True

    def get_top_similar_artists(self, limit=50, profile_id=1):
        return []

    def add_to_discovery_pool(self, track_data, source, profile_id=1):
        self.discovery_pool_calls.append((track_data, source, profile_id))
        return True

    def clear_discovery_recent_albums(self, profile_id=1):
        return True

    def cache_discovery_recent_album(self, album_data, source='spotify', profile_id=1):
        self.discovery_recent_calls.append((album_data, source, profile_id))
        return True

    def cleanup_old_discovery_tracks(self, days_threshold=365):
        return 0

    def update_discovery_pool_timestamp(self, track_count, profile_id=1):
        self.discovery_pool_timestamp_calls.append((track_count, profile_id))
        return True

    class _Cursor:
        def __init__(self, parent):
            self.parent = parent

        def execute(self, *args, **kwargs):
            return None

        def fetchall(self):
            return list(self.parent.db_albums)

        def fetchone(self):
            return {"count": 0}

    class _Conn:
        def __init__(self, cursor):
            self._cursor = cursor

        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc, tb):
            return False

        def cursor(self):
            return self._cursor

    def _get_connection(self):
        return self._Conn(self._Cursor(self))


def _build_artist(name="Artist One", profile_id=11):
    return types.SimpleNamespace(
        artist_name=name,
        spotify_artist_id="sp-artist",
        itunes_artist_id="it-artist",
        deezer_artist_id="dz-artist",
        discogs_artist_id="dg-artist",
        last_scan_timestamp=None,
        id=123,
        profile_id=profile_id,
        include_albums=True,
        include_eps=True,
        include_singles=True,
        include_live=False,
        include_remixes=False,
        include_acoustic=False,
        include_compilations=False,
        include_instrumentals=False,
        lookback_days=7,
        image_url=None,
    )


def _build_scanner(album_data, artists):
    scanner = WatchlistScanner(metadata_service=_FakeMetadataService(album_data))
    scanner._database = _FakeDB(artists)
    scanner._wishlist_service = types.SimpleNamespace()
    scanner._matching_engine = types.SimpleNamespace()
    return scanner


def test_fetch_similar_artists_from_musicmap_uses_provider_priority(monkeypatch):
    html = """
    <html>
      <body>
        <div id="gnodMap">
          <a href="/artist/seed">Artist One</a>
          <a href="/artist/similar">Similar Artist</a>
        </div>
      </body>
    </html>
    """

    class _Response:
        def __init__(self, text):
            self.text = text

        def raise_for_status(self):
            return None

    def make_client(source, seed_id, match_id, canonical_name, popularity):
        client = _FakeSourceClient(artist_id=match_id, albums=[], image_url=None)

        def search_artists(query, limit=1, **kwargs):
            client.search_calls.append((query, limit, kwargs))
            if query == "Artist One":
                return [types.SimpleNamespace(id=seed_id, name=f"{source} Seed")]
            if query == "Similar Artist":
                return [
                    types.SimpleNamespace(
                        id=match_id,
                        name=canonical_name,
                        image_url=f"https://{source}.example.com/{match_id}.jpg",
                        genres=[source, "genre"],
                        popularity=popularity,
                    )
                ]
            return []

        client.search_artists = search_artists
        return client

    deezer_client = make_client("deezer", "dz-seed", "dz-match", "Deezer Canonical", 30)
    itunes_client = make_client("itunes", "it-seed", "it-match", "iTunes Canonical", 20)
    spotify_client = make_client("spotify", "sp-seed", "sp-match", "Spotify Canonical", 10)

    monkeypatch.setattr(watchlist_scanner_module.requests, "get", lambda *args, **kwargs: _Response(html))
    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(
        watchlist_scanner_module,
        "get_client_for_source",
        lambda source: {
            "deezer": deezer_client,
            "itunes": itunes_client,
            "spotify": spotify_client,
        }.get(source),
    )

    scanner = _build_scanner({"tracks": {"items": []}}, [])
    results = scanner._fetch_similar_artists_from_musicmap("Artist One", limit=5)

    assert len(results) == 1
    artist = results[0]
    assert artist["name"] == "Deezer Canonical"
    assert artist["deezer_id"] == "dz-match"
    assert artist["itunes_id"] == "it-match"
    assert artist["spotify_id"] == "sp-match"
    assert artist["image_url"] == "https://deezer.example.com/dz-match.jpg"
    assert artist["genres"] == ["deezer", "genre"]
    assert artist["popularity"] == 30

    assert [call[0] for call in deezer_client.search_calls] == ["Artist One", "Similar Artist"]
    assert [call[0] for call in itunes_client.search_calls] == ["Artist One", "Similar Artist"]
    assert [call[0] for call in spotify_client.search_calls] == ["Artist One", "Similar Artist"]
    assert spotify_client.search_calls[-1][2]["allow_fallback"] is False


def test_backfill_similar_artists_fallback_ids_uses_provider_priority(monkeypatch):
    def make_client(source):
        client = types.SimpleNamespace(search_calls=[])

        def search_artists(query, limit=1, **kwargs):
            client.search_calls.append((query, limit, kwargs))
            safe_name = query.lower().replace(" ", "-")
            return [types.SimpleNamespace(id=f"{source}-{safe_name}", name=query)]

        client.search_artists = search_artists
        return client

    deezer_client = make_client("deezer")
    itunes_client = make_client("itunes")

    deezer_artist = types.SimpleNamespace(id=11, similar_artist_name="Deezer Artist")
    itunes_artist = types.SimpleNamespace(id=22, similar_artist_name="iTunes Artist")

    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(
        watchlist_scanner_module,
        "get_client_for_source",
        lambda source: {
            "deezer": deezer_client,
            "itunes": itunes_client,
        }.get(source),
    )

    scanner = _build_scanner({"tracks": {"items": []}}, [])
    scanner.database.get_similar_artists_missing_fallback_ids = (
        lambda source_artist_id, fallback_source, profile_id=1: [deezer_artist] if fallback_source == "deezer" else [itunes_artist]
    )

    update_calls = []
    scanner.database.update_similar_artist_deezer_id = lambda similar_artist_id, deezer_id: update_calls.append(("deezer", similar_artist_id, deezer_id)) or True
    scanner.database.update_similar_artist_itunes_id = lambda similar_artist_id, itunes_id: update_calls.append(("itunes", similar_artist_id, itunes_id)) or True

    count = scanner._backfill_similar_artists_fallback_ids("source-artist", profile_id=7)

    assert count == 2
    assert update_calls == [
        ("deezer", 11, "deezer-deezer-artist"),
        ("itunes", 22, "itunes-itunes-artist"),
    ]
    assert [call[0] for call in deezer_client.search_calls] == ["Deezer Artist"]
    assert [call[0] for call in itunes_client.search_calls] == ["iTunes Artist"]


def test_scan_watchlist_profile_loads_artists_and_forwards_override_flag(monkeypatch):
    # Global-override application moved from scan_watchlist_profile into
    # scan_watchlist_artists so every entry point (including the automation
    # auto-scan) respects it. scan_watchlist_profile now just forwards the
    # apply_global_overrides flag through.
    artist = _build_artist()
    scanner = _build_scanner({"tracks": {"items": []}}, [artist])

    loaded_profiles = []
    scan_calls = []

    monkeypatch.setattr(scanner.database, "get_watchlist_artists", lambda profile_id=None: loaded_profiles.append(profile_id) or [artist])
    monkeypatch.setattr(scanner, "scan_watchlist_artists", lambda artists, **kwargs: scan_calls.append((list(artists), kwargs)) or ["ok"])

    result = scanner.scan_watchlist_profile(42)

    assert result == ["ok"]
    assert loaded_profiles == [42]
    assert scan_calls and scan_calls[0][0][0].artist_name == "Artist One"
    assert scan_calls[0][1]["profile_id"] == 42
    assert scan_calls[0][1]["apply_global_overrides"] is True


def test_scan_watchlist_artists_applies_global_overrides(monkeypatch):
    # Regression guard: auto-watchlist automation bypassed overrides by
    # calling scan_watchlist_artists directly. Overrides now run inside
    # scan_watchlist_artists so every caller gets the global filter.
    scanner = _build_scanner({"tracks": {"items": []}}, [])

    override_calls = []
    monkeypatch.setattr(scanner, "_apply_global_watchlist_overrides", lambda artists: override_calls.append(list(artists)))

    # Empty artist list exits early but override must still have been invoked.
    scanner.scan_watchlist_artists([])

    assert override_calls == [[]]


def test_scan_watchlist_artists_skips_overrides_when_flag_false(monkeypatch):
    scanner = _build_scanner({"tracks": {"items": []}}, [])

    override_calls = []
    monkeypatch.setattr(scanner, "_apply_global_watchlist_overrides", lambda artists: override_calls.append(list(artists)))

    scanner.scan_watchlist_artists([], apply_global_overrides=False)

    assert override_calls == []


def test_scan_watchlist_artists_scans_tracks_and_updates_state(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ARTISTS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ALBUMS", 0)

    artist = _build_artist()
    album = types.SimpleNamespace(id="album-1", name="Album One")
    album_data = {
        "name": "Album One",
        "images": [{"url": "https://example.com/album.jpg"}],
        "tracks": {
            "items": [
                {
                    "id": "track-1",
                    "name": "Track One",
                    "track_number": 1,
                    "disc_number": 1,
                    "artists": [{"name": "Artist One"}],
                }
            ]
        },
    }
    scanner = _build_scanner(album_data, [artist])
    scanner._database.has_fresh_similar_artists = lambda *args, **kwargs: False

    monkeypatch.setattr(scanner, "_backfill_missing_ids", lambda *args, **kwargs: None)
    monkeypatch.setattr(scanner, "get_artist_image_url", lambda *_args, **_kwargs: "https://example.com/artist.jpg")
    monkeypatch.setattr(scanner, "get_artist_discography_for_watchlist", lambda *_args, **_kwargs: [album])
    monkeypatch.setattr(scanner, "_get_lookback_period_setting", lambda: "30")
    monkeypatch.setattr(scanner, "_get_rescan_cutoff", lambda: None)
    monkeypatch.setattr(scanner, "_should_include_release", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "_should_include_track", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "is_track_missing_from_library", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "add_track_to_wishlist", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "update_artist_scan_timestamp", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "update_similar_artists", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "_backfill_similar_artists_fallback_ids", lambda *_args, **_kwargs: 0)

    scan_state = {}
    results = scanner.scan_watchlist_artists([artist], scan_state=scan_state)

    assert len(results) == 1
    assert results[0].success is True
    assert results[0].new_tracks_found == 1
    assert results[0].tracks_added_to_wishlist == 1
    assert scan_state["status"] == "completed"
    assert scan_state["summary"]["successful_scans"] == 1
    assert scan_state["summary"]["new_tracks_found"] == 1
    assert scan_state["summary"]["tracks_added_to_wishlist"] == 1
    assert scan_state["recent_wishlist_additions"][0]["track_name"] == "Track One"


def test_scan_watchlist_artists_skips_placeholder_tracklists(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ARTISTS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ALBUMS", 0)

    artist = _build_artist()
    album = types.SimpleNamespace(id="album-1", name="Album One")
    album_data = {
        "name": "Album One",
        "images": [{"url": "https://example.com/album.jpg"}],
        "tracks": {
            "items": [
                {
                    "id": "track-1",
                    "name": "Track 1",
                    "track_number": 1,
                    "disc_number": 1,
                    "artists": [{"name": "Artist One"}],
                },
                {
                    "id": "track-2",
                    "name": "Track 2",
                    "track_number": 2,
                    "disc_number": 1,
                    "artists": [{"name": "Artist One"}],
                },
            ]
        },
    }
    scanner = _build_scanner(album_data, [artist])
    scanner._database.has_fresh_similar_artists = lambda *args, **kwargs: False

    monkeypatch.setattr(scanner, "_backfill_missing_ids", lambda *args, **kwargs: None)
    monkeypatch.setattr(scanner, "get_artist_image_url", lambda *_args, **_kwargs: "https://example.com/artist.jpg")
    monkeypatch.setattr(scanner, "get_artist_discography_for_watchlist", lambda *_args, **_kwargs: [album])
    monkeypatch.setattr(scanner, "_get_lookback_period_setting", lambda: "30")
    monkeypatch.setattr(scanner, "_get_rescan_cutoff", lambda: None)
    monkeypatch.setattr(scanner, "_should_include_release", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "_should_include_track", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "is_track_missing_from_library", lambda *_args, **_kwargs: True)

    add_calls = []
    monkeypatch.setattr(scanner, "add_track_to_wishlist", lambda *args, **kwargs: add_calls.append((args, kwargs)) or True)
    monkeypatch.setattr(scanner, "update_artist_scan_timestamp", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "update_similar_artists", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "_backfill_similar_artists_fallback_ids", lambda *_args, **_kwargs: 0)

    scan_state = {}
    results = scanner.scan_watchlist_artists([artist], scan_state=scan_state)

    assert len(results) == 1
    assert results[0].success is True
    assert results[0].new_tracks_found == 0
    assert results[0].tracks_added_to_wishlist == 0
    assert add_calls == []
    assert scan_state["summary"]["new_tracks_found"] == 0
    assert scan_state["summary"]["tracks_added_to_wishlist"] == 0


def test_scan_watchlist_artists_honors_cancel_check(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ARTISTS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ALBUMS", 0)

    artist_a = _build_artist("Artist One")
    artist_b = _build_artist("Artist Two")
    album = types.SimpleNamespace(id="album-1", name="Album One")
    album_data = {
        "name": "Album One",
        "tracks": {"items": []},
    }
    scanner = _build_scanner(album_data, [artist_a, artist_b])
    scanner._database.has_fresh_similar_artists = lambda *args, **kwargs: False

    monkeypatch.setattr(scanner, "_backfill_missing_ids", lambda *args, **kwargs: None)
    monkeypatch.setattr(scanner, "get_artist_image_url", lambda *_args, **_kwargs: "https://example.com/artist.jpg")
    monkeypatch.setattr(scanner, "get_artist_discography_for_watchlist", lambda *_args, **_kwargs: [album])
    monkeypatch.setattr(scanner, "_get_lookback_period_setting", lambda: "30")
    monkeypatch.setattr(scanner, "_get_rescan_cutoff", lambda: None)
    monkeypatch.setattr(scanner, "_should_include_release", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "_should_include_track", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "is_track_missing_from_library", lambda *_args, **_kwargs: False)
    monkeypatch.setattr(scanner, "add_track_to_wishlist", lambda *_args, **_kwargs: False)
    monkeypatch.setattr(scanner, "update_artist_scan_timestamp", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "update_similar_artists", lambda *_args, **_kwargs: True)
    monkeypatch.setattr(scanner, "_backfill_similar_artists_fallback_ids", lambda *_args, **_kwargs: 0)

    cancels = iter([False, True])
    scan_state = {}
    results = scanner.scan_watchlist_artists(
        [artist_a, artist_b],
        scan_state=scan_state,
        cancel_check=lambda: next(cancels),
    )

    assert len(results) == 1
    assert results[0].artist_name == "Artist One"
    assert scan_state["status"] == "cancelled"
    assert scan_state["summary"]["cancelled"] is True
    assert scan_state["summary"]["successful_scans"] == 1


def test_get_artist_discography_for_watchlist_prefers_primary_source(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "time", types.SimpleNamespace(sleep=lambda *_args, **_kwargs: None))
    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(watchlist_scanner_module, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])

    deezer_album = types.SimpleNamespace(id="dz-album", name="Deezer Album", release_date=None)
    spotify_album = types.SimpleNamespace(id="sp-album", name="Spotify Album", release_date=None)

    deezer_client = _FakeSourceClient(artist_id="dz-artist", albums=[deezer_album], image_url="https://example.com/deezer.jpg")
    spotify_client = _FakeSourceClient(artist_id="sp-artist", albums=[spotify_album], image_url="https://example.com/spotify.jpg")

    def fake_get_client_for_source(source):
        return {"deezer": deezer_client, "spotify": spotify_client}.get(source)

    monkeypatch.setattr(watchlist_scanner_module, "get_client_for_source", fake_get_client_for_source)

    artist = _build_artist()
    artist.spotify_artist_id = "sp-artist"
    artist.deezer_artist_id = "dz-artist"

    scanner = _build_scanner({"tracks": {"items": []}}, [artist])
    scanner._database.has_fresh_similar_artists = lambda *args, **kwargs: False
    scanner._get_lookback_period_setting = lambda: "30"
    scanner._get_rescan_cutoff = lambda: None

    result = scanner.get_artist_discography_for_watchlist(artist, None)

    assert result is not None
    assert result.source == "deezer"
    assert result.artist_id == "dz-artist"
    assert result.albums and result.albums[0].id == "dz-album"
    assert deezer_client.album_calls
    assert spotify_client.album_calls == []


def test_get_artist_discography_for_watchlist_falls_back_when_primary_fails(monkeypatch):
    """When the primary source API fails (returns None), fall back to next source."""
    monkeypatch.setattr(watchlist_scanner_module, "time", types.SimpleNamespace(sleep=lambda *_args, **_kwargs: None))
    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(watchlist_scanner_module, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])

    # Deezer client returns None from get_artist_albums (API failure)
    deezer_client = _FakeSourceClient(artist_id="dz-artist", albums=[], image_url="https://example.com/deezer.jpg")
    deezer_client.get_artist_albums = lambda *args, **kwargs: None  # Simulate API failure
    spotify_album = types.SimpleNamespace(id="sp-album", name="Spotify Album", release_date=None)
    spotify_client = _FakeSourceClient(artist_id="sp-artist", albums=[spotify_album], image_url="https://example.com/spotify.jpg")

    def fake_get_client_for_source(source):
        return {"deezer": deezer_client, "spotify": spotify_client}.get(source)

    monkeypatch.setattr(watchlist_scanner_module, "get_client_for_source", fake_get_client_for_source)

    artist = _build_artist()
    artist.spotify_artist_id = "sp-artist"
    artist.deezer_artist_id = "dz-artist"

    scanner = _build_scanner({"tracks": {"items": []}}, [artist])
    scanner._database.has_fresh_similar_artists = lambda *args, **kwargs: False
    scanner._get_lookback_period_setting = lambda: "30"
    scanner._get_rescan_cutoff = lambda: None

    result = scanner.get_artist_discography_for_watchlist(artist, None)

    assert result is not None
    assert result.source == "spotify"
    assert result.artist_id == "sp-artist"
    assert result.albums and result.albums[0].id == "sp-album"
    # Spotify client should have been called as fallback
    assert spotify_client.album_calls


def test_populate_discovery_pool_uses_primary_source_first(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ARTISTS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ALBUMS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "time", types.SimpleNamespace(sleep=lambda *_args, **_kwargs: None))
    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(watchlist_scanner_module, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])

    similar_artist = types.SimpleNamespace(
        id=501,
        similar_artist_name="Similar Artist",
        occurrence_count=3,
        similar_artist_spotify_id="sp-artist",
        similar_artist_itunes_id="it-artist",
        similar_artist_deezer_id="dz-artist",
    )

    album = types.SimpleNamespace(id="dz-album-1", name="Deezer Album", album_type="album")
    deezer_album_payload = {
        "id": "dz-album-1",
        "name": "Deezer Album",
        "images": [{"url": "https://example.com/deezer-album.jpg"}],
        "release_date": "2026-04-01",
        "popularity": 0,
        "tracks": {
            "items": [
                {
                    "id": "dz-track-1",
                    "name": "Deezer Track",
                    "duration_ms": 123456,
                    "artists": [{"name": "Similar Artist"}],
                }
            ]
        },
        "artists": [{"id": "dz-artist"}],
    }

    deezer_client = _FakeSourceClient(
        artist_id="dz-artist",
        albums=[album],
        image_url="https://example.com/deezer-artist.jpg",
        album_payload=deezer_album_payload,
    )
    spotify_client = _FakeSourceClient(
        artist_id="sp-artist",
        albums=[types.SimpleNamespace(id="sp-album-1", name="Spotify Album", album_type="album")],
        image_url="https://example.com/spotify-artist.jpg",
        album_payload={
            "id": "sp-album-1",
            "name": "Spotify Album",
            "images": [{"url": "https://example.com/spotify-album.jpg"}],
            "release_date": "2026-04-01",
            "popularity": 50,
            "tracks": {"items": []},
            "artists": [{"id": "sp-artist"}],
        },
    )

    def fake_get_client_for_source(source):
        return {
            "deezer": deezer_client,
            "spotify": spotify_client,
        }.get(source)

    monkeypatch.setattr(watchlist_scanner_module, "get_client_for_source", fake_get_client_for_source)

    scanner = _build_scanner({"tracks": {"items": []}}, [])
    scanner._database.has_fresh_similar_artists = lambda *args, **kwargs: False
    scanner.database.should_populate_discovery_pool = lambda hours_threshold=24, profile_id=1: True
    scanner.database.get_top_similar_artists = lambda limit=50, profile_id=1: [similar_artist]
    scanner.database.db_albums = []
    scanner.cache_discovery_recent_albums = lambda *args, **kwargs: None
    scanner.curate_discovery_playlists = lambda *args, **kwargs: None
    scanner.database.update_discovery_pool_timestamp = lambda *args, **kwargs: True
    scanner.database.cleanup_old_discovery_tracks = lambda *args, **kwargs: 0

    scanner.populate_discovery_pool(top_artists_limit=1, albums_per_artist=1, profile_id=1)

    assert scanner.database.discovery_pool_calls
    assert scanner.database.discovery_pool_calls[0][1] == "deezer"
    assert deezer_client.album_calls
    assert spotify_client.search_calls == []
    assert spotify_client.artist_calls == []


def test_populate_discovery_pool_falls_back_to_spotify_when_primary_has_no_albums(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ARTISTS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ALBUMS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "time", types.SimpleNamespace(sleep=lambda *_args, **_kwargs: None))
    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(watchlist_scanner_module, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])

    similar_artist = types.SimpleNamespace(
        id=502,
        similar_artist_name="Fallback Artist",
        occurrence_count=1,
        similar_artist_spotify_id="sp-artist",
        similar_artist_itunes_id="it-artist",
        similar_artist_deezer_id="dz-artist",
    )

    deezer_client = _FakeSourceClient(
        artist_id="dz-artist",
        albums=[],
        image_url="https://example.com/deezer-artist.jpg",
    )
    spotify_album = types.SimpleNamespace(id="sp-album-1", name="Spotify Album", album_type="album")
    spotify_client = _FakeSourceClient(
        artist_id="sp-artist",
        albums=[spotify_album],
        image_url="https://example.com/spotify-artist.jpg",
        album_payload={
            "id": "sp-album-1",
            "name": "Spotify Album",
            "images": [{"url": "https://example.com/spotify-album.jpg"}],
            "release_date": "2026-04-01",
            "popularity": 50,
            "tracks": {
                "items": [
                    {
                        "id": "sp-track-1",
                        "name": "Spotify Track",
                        "duration_ms": 234567,
                        "artists": [{"name": "Fallback Artist"}],
                    }
                ]
            },
            "artists": [{"id": "sp-artist"}],
        },
    )

    def fake_get_client_for_source(source):
        return {
            "deezer": deezer_client,
            "spotify": spotify_client,
        }.get(source)

    monkeypatch.setattr(watchlist_scanner_module, "get_client_for_source", fake_get_client_for_source)

    scanner = _build_scanner({"tracks": {"items": []}}, [])
    scanner._database.has_fresh_similar_artists = lambda *args, **kwargs: False
    scanner.database.should_populate_discovery_pool = lambda hours_threshold=24, profile_id=1: True
    scanner.database.get_top_similar_artists = lambda limit=50, profile_id=1: [similar_artist]
    scanner.database.db_albums = []
    scanner.cache_discovery_recent_albums = lambda *args, **kwargs: None
    scanner.curate_discovery_playlists = lambda *args, **kwargs: None
    scanner.database.update_discovery_pool_timestamp = lambda *args, **kwargs: True
    scanner.database.cleanup_old_discovery_tracks = lambda *args, **kwargs: 0

    scanner.populate_discovery_pool(top_artists_limit=1, albums_per_artist=1, profile_id=1)

    assert scanner.database.discovery_pool_calls
    assert scanner.database.discovery_pool_calls[0][1] == "spotify"
    assert deezer_client.album_calls
    assert spotify_client.search_calls == [("Fallback Artist", 1, {"allow_fallback": False})]
    assert spotify_client.album_calls
    assert any(
        isinstance(call, tuple)
        and len(call) == 4
        and call[0] == "sp-artist"
        and call[3].get("skip_cache") is False
        and call[3].get("allow_fallback") is False
        and call[3].get("max_pages") == 2
        for call in spotify_client.album_calls
    )
    assert any(
        isinstance(call, tuple)
        and len(call) == 4
        and call[3].get("allow_fallback") is False
        for call in spotify_client.album_calls
    )


def test_populate_discovery_pool_uses_strict_spotify_for_database_album_search(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ARTISTS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ALBUMS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "time", types.SimpleNamespace(sleep=lambda *_args, **_kwargs: None))
    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(watchlist_scanner_module, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])

    similar_artist = types.SimpleNamespace(
        id=503,
        similar_artist_name="No Album Artist",
        occurrence_count=1,
        similar_artist_spotify_id="sp-artist",
        similar_artist_itunes_id="it-artist",
        similar_artist_deezer_id="dz-artist",
    )

    deezer_client = _FakeSourceClient(
        artist_id="dz-artist",
        albums=[],
        image_url="https://example.com/deezer-artist.jpg",
    )
    spotify_client = _FakeSourceClient(
        artist_id="sp-artist",
        albums=[],
        image_url="https://example.com/spotify-artist.jpg",
        album_search_results=[types.SimpleNamespace(id="sp-db-album", name="DB Album")],
        album_payload={
            "id": "sp-db-album",
            "name": "DB Album",
            "images": [{"url": "https://example.com/db-album.jpg"}],
            "release_date": "2026-04-01",
            "popularity": 75,
            "tracks": {
                "items": [
                    {
                        "id": "sp-db-track-1",
                        "name": "DB Track",
                        "duration_ms": 345678,
                        "artists": [{"name": "DB Artist"}],
                    }
                ]
            },
            "artists": [{"id": "sp-artist"}],
        },
    )

    def fake_get_client_for_source(source):
        return {
            "deezer": deezer_client,
            "spotify": spotify_client,
        }.get(source)

    monkeypatch.setattr(watchlist_scanner_module, "get_client_for_source", fake_get_client_for_source)

    scanner = _build_scanner({"tracks": {"items": []}}, [])
    scanner._database.has_fresh_similar_artists = lambda *args, **kwargs: False
    scanner.database.should_populate_discovery_pool = lambda hours_threshold=24, profile_id=1: True
    scanner.database.get_top_similar_artists = lambda limit=50, profile_id=1: [similar_artist]
    scanner.database.db_albums = [{"title": "DB Album", "artist_name": "DB Artist"}]
    scanner.cache_discovery_recent_albums = lambda *args, **kwargs: None
    scanner.curate_discovery_playlists = lambda *args, **kwargs: None
    scanner.database.update_discovery_pool_timestamp = lambda *args, **kwargs: True
    scanner.database.cleanup_old_discovery_tracks = lambda *args, **kwargs: 0

    scanner.populate_discovery_pool(top_artists_limit=1, albums_per_artist=1, profile_id=1)

    assert scanner.database.discovery_pool_calls
    assert scanner.database.discovery_pool_calls[0][1] == "spotify"
    assert spotify_client.search_album_calls
    assert any(
        kwargs.get("allow_fallback") is False
        for _, _, kwargs in spotify_client.search_album_calls
    )
    assert any(
        isinstance(call, tuple)
        and len(call) == 4
        and call[0] == "sp-artist"
        and call[3].get("skip_cache") is False
        and call[3].get("allow_fallback") is False
        and call[3].get("max_pages") == 2
        for call in spotify_client.album_calls
    )
    assert any(
        isinstance(call, tuple)
        and len(call) == 2
        and call[1].get("allow_fallback") is False
        for call in spotify_client.album_calls
        if len(call) == 2
    )


def test_cache_discovery_recent_albums_uses_primary_source_first(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ARTISTS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "time", types.SimpleNamespace(sleep=lambda *_args, **_kwargs: None))
    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(watchlist_scanner_module, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])

    artist = _build_artist("Artist One")
    album = types.SimpleNamespace(
        id="dz-album-1",
        name="Recent Deezer Album",
        album_type="album",
        release_date=_RECENT_RELEASE_DATE,
        image_url="https://example.com/deezer-album.jpg",
    )

    deezer_client = _FakeSourceClient(
        artist_id="dz-artist",
        albums=[album],
        image_url="https://example.com/deezer-artist.jpg",
    )
    spotify_client = _FakeSourceClient(
        artist_id="sp-artist",
        albums=[types.SimpleNamespace(id="sp-album-1", name="Spotify Album", album_type="album")],
        image_url="https://example.com/spotify-artist.jpg",
    )

    def fake_get_client_for_source(source):
        return {
            "deezer": deezer_client,
            "spotify": spotify_client,
        }.get(source)

    monkeypatch.setattr(watchlist_scanner_module, "get_client_for_source", fake_get_client_for_source)

    scanner = _build_scanner({"tracks": {"items": []}}, [artist])
    scanner.database.get_top_similar_artists = lambda limit=50, profile_id=1: []

    scanner.cache_discovery_recent_albums(profile_id=1)

    assert scanner.database.discovery_recent_calls
    assert scanner.database.discovery_recent_calls[0][1] == "deezer"
    assert deezer_client.album_calls
    assert spotify_client.search_calls == []
    assert spotify_client.album_calls == []


def test_cache_discovery_recent_albums_falls_back_to_spotify_when_primary_has_no_albums(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ARTISTS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "time", types.SimpleNamespace(sleep=lambda *_args, **_kwargs: None))
    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(watchlist_scanner_module, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])

    artist = _build_artist("Fallback Artist")
    artist.spotify_artist_id = None
    deezer_client = _FakeSourceClient(
        artist_id="dz-artist",
        albums=[],
        image_url="https://example.com/deezer-artist.jpg",
    )
    spotify_album = types.SimpleNamespace(
        id="sp-album-1",
        name="Spotify Recent Album",
        album_type="album",
        release_date=_RECENT_RELEASE_DATE,
        image_url="https://example.com/spotify-album.jpg",
    )
    spotify_client = _FakeSourceClient(
        artist_id="sp-artist",
        albums=[spotify_album],
        image_url="https://example.com/spotify-artist.jpg",
        album_payload={
            "id": "sp-album-1",
            "name": "Spotify Recent Album",
            "images": [{"url": "https://example.com/spotify-album.jpg"}],
            "release_date": _RECENT_RELEASE_DATE,
            "popularity": 50,
            "tracks": {"items": [{"id": "sp-track-1", "name": "Spotify Track", "artists": [{"name": "Fallback Artist"}]}]},
            "artists": [{"id": "sp-artist"}],
        },
    )

    def fake_get_client_for_source(source):
        return {
            "deezer": deezer_client,
            "spotify": spotify_client,
        }.get(source)

    monkeypatch.setattr(watchlist_scanner_module, "get_client_for_source", fake_get_client_for_source)

    scanner = _build_scanner({"tracks": {"items": []}}, [artist])
    scanner.database.get_top_similar_artists = lambda limit=50, profile_id=1: []

    scanner.cache_discovery_recent_albums(profile_id=1)

    assert scanner.database.discovery_recent_calls
    assert scanner.database.discovery_recent_calls[0][1] == "spotify"
    assert deezer_client.album_calls
    assert spotify_client.search_calls == [("Fallback Artist", 1, {"allow_fallback": False})]
    assert spotify_client.album_calls


def test_update_discovery_pool_incremental_uses_source_priority(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ARTISTS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "time", types.SimpleNamespace(sleep=lambda *_args, **_kwargs: None))
    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(watchlist_scanner_module, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])

    artist = _build_artist("Incremental Artist")
    artist.spotify_artist_id = None
    artist.deezer_artist_id = None

    release = types.SimpleNamespace(
        id="dz-release-1",
        name="Incremental Release",
        release_date=_RECENT_RELEASE_DATE,
        album_type="album",
        image_url="https://example.com/deezer-release.jpg",
    )

    deezer_client = _FakeSourceClient(
        artist_id="dz-artist",
        albums=[release],
        image_url="https://example.com/deezer-artist.jpg",
        album_payload={
            "id": "dz-release-1",
            "name": "Incremental Release",
            "images": [{"url": "https://example.com/deezer-release.jpg"}],
            "release_date": _RECENT_RELEASE_DATE,
            "popularity": 10,
            "tracks": {"items": [{"id": "dz-track-1", "name": "Incremental Track", "artists": [{"name": "Incremental Artist"}], "duration_ms": 180000}]},
            "artists": [{"id": "dz-artist"}],
        },
    )
    spotify_client = _FakeSourceClient(
        artist_id="sp-artist",
        albums=[],
        image_url="https://example.com/spotify-artist.jpg",
        album_payload={
            "id": "sp-release-1",
            "name": "Spotify Incremental Release",
            "images": [{"url": "https://example.com/spotify-release.jpg"}],
            "release_date": _RECENT_RELEASE_DATE,
            "popularity": 50,
            "tracks": {"items": [{"id": "sp-track-1", "name": "Spotify Incremental Track", "artists": [{"name": "Incremental Artist"}], "duration_ms": 180000}]},
            "artists": [{"id": "sp-artist"}],
        },
    )

    def fake_get_client_for_source(source):
        return {
            "deezer": deezer_client,
            "spotify": spotify_client,
        }.get(source)

    monkeypatch.setattr(watchlist_scanner_module, "get_client_for_source", fake_get_client_for_source)

    scanner = _build_scanner({"tracks": {"items": []}}, [artist])
    scanner.database.should_populate_discovery_pool = lambda hours_threshold=6, profile_id=1: True

    scanner.update_discovery_pool_incremental(profile_id=1)

    assert scanner.database.discovery_pool_calls
    assert scanner.database.discovery_pool_calls[0][1] == "deezer"
    assert scanner.database.discovery_pool_calls[0][0]["deezer_track_id"] == "dz-track-1"
    assert scanner.database.discovery_pool_calls[0][0]["deezer_album_id"] == "dz-release-1"
    assert scanner.database.discovery_pool_calls[0][0]["deezer_artist_id"] == "dz-artist"
    assert scanner.database.discovery_pool_calls[0][0].get("spotify_track_id") is None
    assert deezer_client.search_calls == [("Incremental Artist", 1, {})]
    assert deezer_client.album_calls
    assert spotify_client.search_calls == []
    assert spotify_client.album_calls == []


def test_curate_discovery_playlists_uses_source_priority_for_recent_albums(monkeypatch):
    monkeypatch.setattr(watchlist_scanner_module, "DELAY_BETWEEN_ARTISTS", 0)
    monkeypatch.setattr(watchlist_scanner_module, "get_primary_source", lambda: "deezer")
    monkeypatch.setattr(watchlist_scanner_module, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])

    artist = _build_artist("Playlist Artist")
    scanner = _build_scanner({"tracks": {"items": []}}, [artist])

    saved_playlists = []
    recent_album = {
        "album_deezer_id": "dz-album-1",
        "album_itunes_id": None,
        "album_spotify_id": None,
        "album_name": "Recent Deezer Album",
        "artist_name": "Playlist Artist",
        "release_date": "2026-04-01",
        "album_type": "album",
        "album_cover_url": "https://example.com/deezer-album.jpg",
        "artist_deezer_id": "dz-artist",
        "artist_spotify_id": None,
        "artist_itunes_id": None,
    }
    discovery_track = types.SimpleNamespace(
        artist_name="Playlist Artist",
        popularity=72,
        deezer_track_id="dz-track-1",
        spotify_track_id=None,
        itunes_track_id=None,
    )
    deezer_client = _FakeSourceClient(
        artist_id="dz-artist",
        albums=[],
        image_url="https://example.com/deezer-artist.jpg",
        album_payload={
            "id": "dz-album-1",
            "name": "Recent Deezer Album",
            "images": [{"url": "https://example.com/deezer-album.jpg"}],
            "release_date": "2026-04-01",
            "popularity": 40,
            "tracks": {"items": [{"id": "dz-track-1", "name": "Track One", "artists": [{"name": "Playlist Artist"}], "duration_ms": 180000}]},
            "artists": [{"id": "dz-artist"}],
        },
    )
    spotify_client = _FakeSourceClient(
        artist_id="sp-artist",
        albums=[],
        image_url="https://example.com/spotify-artist.jpg",
        album_payload={
            "id": "sp-album-1",
            "name": "Spotify Album",
            "images": [{"url": "https://example.com/spotify-album.jpg"}],
            "release_date": "2026-04-01",
            "popularity": 60,
            "tracks": {"items": [{"id": "sp-track-1", "name": "Spotify Track", "artists": [{"name": "Playlist Artist"}], "duration_ms": 180000}]},
            "artists": [{"id": "sp-artist"}],
        },
    )

    def fake_get_client_for_source(source):
        return {
            "deezer": deezer_client,
            "spotify": spotify_client,
        }.get(source)

    monkeypatch.setattr(watchlist_scanner_module, "get_client_for_source", fake_get_client_for_source)
    monkeypatch.setattr(scanner, "_get_listening_profile", lambda profile_id: {
        "has_data": False,
        "top_artist_names": set(),
        "top_genres": set(),
        "avg_daily_plays": 0.0,
        "artist_play_counts": {},
    })
    monkeypatch.setattr(scanner.database, "get_discovery_recent_albums", lambda limit, source, profile_id: [recent_album] if source == "deezer" else [], raising=False)
    monkeypatch.setattr(scanner.database, "get_discovery_pool_tracks", lambda *args, **kwargs: [discovery_track] if kwargs.get("source") == "deezer" else [], raising=False)
    monkeypatch.setattr(scanner.database, "save_curated_playlist", lambda key, tracks, profile_id=1: saved_playlists.append((key, list(tracks))) or True, raising=False)
    monkeypatch.setattr(scanner.database, "get_top_artists", lambda *args, **kwargs: [], raising=False)
    monkeypatch.setattr(scanner.database, "get_watchlist_artists", lambda *args, **kwargs: [], raising=False)

    scanner.curate_discovery_playlists(profile_id=1)

    assert any(call[0] == "dz-album-1" for call in deezer_client.album_calls)
    assert spotify_client.album_calls == []
    assert any(key == "release_radar_deezer" for key, _ in saved_playlists)
    assert any(key == "discovery_weekly_deezer" for key, _ in saved_playlists)


def test_has_fresh_similar_artists_uses_age_only(tmp_path):
    from datetime import datetime
    from database.music_database import MusicDatabase

    db = MusicDatabase(str(tmp_path / "music.db"))
    db.add_or_update_similar_artist(
        source_artist_id="source-1",
        similar_artist_name="Similar Artist",
        similar_artist_itunes_id="it-artist",
        similar_artist_deezer_id="dz-artist",
        profile_id=1,
    )

    with db._get_connection() as conn:
        conn.execute(
            "UPDATE similar_artists SET last_updated = ? WHERE source_artist_id = ? AND profile_id = ?",
            (datetime.now().isoformat(), "source-1", 1),
        )
        conn.commit()

    assert db.has_fresh_similar_artists("source-1", days_threshold=30, profile_id=1) is True


def test_match_to_spotify_uses_strict_lookup():
    spotify_client = _FakeSpotifyClient(
        search_results=[types.SimpleNamespace(id="fallback-id", name="Artist One")]
    )
    scanner = WatchlistScanner(metadata_service=_FakeMetadataService(None, spotify_client=spotify_client))
    original_get_client_for_source = watchlist_scanner_module.get_client_for_source
    watchlist_scanner_module.get_client_for_source = lambda source: spotify_client if source == "spotify" else None

    try:
        result = scanner._match_to_spotify("Artist One")
    finally:
        watchlist_scanner_module.get_client_for_source = original_get_client_for_source

    assert result is None
    assert spotify_client.search_calls == [("Artist One", 5, False)]
