"""Tests for the reorganize-queue DB helpers on `MusicDatabase`:

- ``get_album_display_meta(album_id)`` — returns the title/artist tuple
  the queue uses for status-panel display, or None when not found.
- ``get_artist_albums_for_reorganize(artist_id)`` — returns the
  bulk-enqueue list ordered by year then title.

These are isolated DB-method tests so the SQL itself is verified
without spinning up Flask, the queue worker, or the orchestrator.
"""

import sqlite3
import sys
import types

import pytest


# ── stubs (same shape used elsewhere in the test suite) ───────────────────
if "spotipy" not in sys.modules:
    spotipy = types.ModuleType("spotipy")
    spotipy.Spotify = object
    oauth2 = types.ModuleType("spotipy.oauth2")
    oauth2.SpotifyOAuth = object
    oauth2.SpotifyClientCredentials = object
    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 "primary"

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


from database.music_database import MusicDatabase  # noqa: E402


# ── helpers ───────────────────────────────────────────────────────────────


class _InMemoryDB(MusicDatabase):
    """MusicDatabase that uses an in-memory sqlite that survives across
    `_get_connection()` calls. Lets tests seed rows once and have the
    methods under test see them."""

    def __init__(self):
        # Skip the real __init__ — it would try to migrate a real db.
        self._conn = sqlite3.connect(":memory:")
        self._conn.row_factory = sqlite3.Row

    def _get_connection(self):
        return _NonClosingConn(self._conn)


class _NonClosingConn:
    """Wraps the shared sqlite connection so `with db._get_connection()
    as conn:` doesn't close the underlying handle between calls."""
    def __init__(self, real):
        self._real = real

    def cursor(self):
        return self._real.cursor()

    def commit(self):
        return self._real.commit()

    def close(self):
        pass

    def __enter__(self):
        return self

    def __exit__(self, *args):
        pass


def _seed(db, *, artists=(), albums=()):
    cur = db._conn.cursor()
    cur.execute("CREATE TABLE artists (id TEXT PRIMARY KEY, name TEXT)")
    cur.execute("""
        CREATE TABLE albums (
            id TEXT PRIMARY KEY,
            artist_id TEXT,
            title TEXT,
            year INTEGER
        )
    """)
    for ar in artists:
        cur.execute("INSERT INTO artists VALUES (?, ?)", ar)
    for al in albums:
        cur.execute(
            "INSERT INTO albums (id, artist_id, title, year) VALUES (?, ?, ?, ?)",
            al,
        )
    db._conn.commit()


@pytest.fixture
def db():
    return _InMemoryDB()


# ── get_album_display_meta ────────────────────────────────────────────────


def test_get_album_display_meta_returns_dict_for_known_album(db):
    _seed(db,
          artists=[('ar-1', 'Kendrick Lamar')],
          albums=[('alb-1', 'ar-1', 'good kid, m.A.A.d city', 2012)])
    meta = db.get_album_display_meta('alb-1')
    assert meta == {
        'album_title': 'good kid, m.A.A.d city',
        'artist_id': 'ar-1',
        'artist_name': 'Kendrick Lamar',
    }


def test_get_album_display_meta_returns_none_for_missing_album(db):
    _seed(db, artists=[('ar-1', 'Aerosmith')])
    assert db.get_album_display_meta('does-not-exist') is None


def test_get_album_display_meta_falls_back_for_blank_strings(db):
    """Albums with empty title or artist name in the DB still need a
    safe display value — the queue UI should never render '(blank)'."""
    _seed(db,
          artists=[('ar-1', '')],
          albums=[('alb-1', 'ar-1', '', 2015)])
    meta = db.get_album_display_meta('alb-1')
    assert meta['album_title'] == 'Unknown Album'
    assert meta['artist_name'] == 'Unknown Artist'
    assert meta['artist_id'] == 'ar-1'


# ── get_artist_albums_for_reorganize ──────────────────────────────────────


def test_get_artist_albums_for_reorganize_orders_by_year_then_title(db):
    _seed(db,
          artists=[('ar-1', 'Aerosmith')],
          albums=[
              ('alb-c', 'ar-1', 'Toys in the Attic', 1975),
              ('alb-a', 'ar-1', 'Aerosmith', 1973),
              ('alb-b', 'ar-1', 'Get Your Wings', 1974),
          ])
    rows = db.get_artist_albums_for_reorganize('ar-1')
    assert [r['album_id'] for r in rows] == ['alb-a', 'alb-b', 'alb-c']
    assert all(r['artist_name'] == 'Aerosmith' for r in rows)


def test_get_artist_albums_for_reorganize_secondary_sorts_by_title(db):
    """Same release year → tiebreak on title alphabetically."""
    _seed(db,
          artists=[('ar-1', 'X')],
          albums=[
              ('alb-z', 'ar-1', 'Zebra', 1990),
              ('alb-a', 'ar-1', 'Apple', 1990),
              ('alb-m', 'ar-1', 'Mango', 1990),
          ])
    rows = db.get_artist_albums_for_reorganize('ar-1')
    assert [r['album_title'] for r in rows] == ['Apple', 'Mango', 'Zebra']


def test_get_artist_albums_for_reorganize_returns_empty_for_unknown_artist(db):
    _seed(db, artists=[('ar-1', 'Aerosmith')])
    assert db.get_artist_albums_for_reorganize('not-a-real-artist') == []


def test_get_artist_albums_for_reorganize_isolates_by_artist(db):
    """Pulling albums for artist A must NOT leak in albums from artist B."""
    _seed(db,
          artists=[('ar-1', 'A'), ('ar-2', 'B')],
          albums=[
              ('alb-1', 'ar-1', 'A1', 2000),
              ('alb-2', 'ar-2', 'B1', 2000),
              ('alb-3', 'ar-1', 'A2', 2001),
          ])
    rows = db.get_artist_albums_for_reorganize('ar-1')
    assert {r['album_id'] for r in rows} == {'alb-1', 'alb-3'}


# ── error propagation ────────────────────────────────────────────────────
# Regression for review feedback on the original PR: helpers used to
# swallow every Exception and return None / [], so a real DB outage
# masqueraded as "album not found" / "no albums". Now they let the
# error bubble — the route layer turns it into a 500 — so the user sees
# a real failure instead of a phantom empty state.


def test_get_album_display_meta_propagates_db_errors(db):
    """If the underlying tables don't exist, the helper must raise
    rather than swallow it as a missing-album result."""
    # Don't seed — the schema is empty, so the SELECT will fail with
    # OperationalError ("no such table: albums").
    with pytest.raises(sqlite3.OperationalError):
        db.get_album_display_meta('alb-1')


def test_get_artist_albums_for_reorganize_propagates_db_errors(db):
    with pytest.raises(sqlite3.OperationalError):
        db.get_artist_albums_for_reorganize('ar-1')
