"""Tests for the HiFi instance CRUD helpers on ``MusicDatabase``:

- ``get_hifi_instances()`` — returns enabled instances ordered by priority
- ``get_all_hifi_instances()`` — returns all instances (enabled + disabled)
- ``add_hifi_instance(url, priority)`` — inserts a new instance
- ``remove_hifi_instance(url)`` — deletes an instance by URL
- ``toggle_hifi_instance(url, enabled)`` — enables/disables an instance
- ``reorder_hifi_instances(urls)`` — updates priority ordering
- ``seed_hifi_instances(default_urls)`` — seeds defaults when table is empty

These are isolated DB-method tests so the SQL itself is verified
without spinning up Flask or any HiFi client.
"""

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."""

    def __init__(self):
        self._conn = sqlite3.connect(":memory:")
        self._conn.row_factory = sqlite3.Row
        self._conn.execute("""
            CREATE TABLE hifi_instances (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                url TEXT NOT NULL UNIQUE,
                priority INTEGER NOT NULL DEFAULT 0,
                enabled INTEGER NOT NULL DEFAULT 1,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
        self._conn.commit()

    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


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


def _seed(db, *, instances=()):
    """Seed hifi_instances rows. Each tuple: (url, priority, enabled)."""
    cur = db._conn.cursor()
    for url, priority, enabled in instances:
        cur.execute(
            "INSERT INTO hifi_instances (url, priority, enabled) VALUES (?, ?, ?)",
            (url, priority, enabled),
        )
    db._conn.commit()


# ── get_hifi_instances ────────────────────────────────────────────────────


def test_get_hifi_instances_returns_enabled_ordered_by_priority(db):
    _seed(db, instances=[
        ("http://b.com", 10, 1),
        ("http://a.com", 5, 1),
        ("http://c.com", 1, 1),
    ])
    rows = db.get_hifi_instances()
    assert [r["url"] for r in rows] == ["http://c.com", "http://a.com", "http://b.com"]
    assert [r["priority"] for r in rows] == [1, 5, 10]


def test_get_hifi_instances_excludes_disabled(db):
    _seed(db, instances=[
        ("http://a.com", 0, 1),
        ("http://b.com", 1, 0),
        ("http://c.com", 2, 1),
    ])
    rows = db.get_hifi_instances()
    assert {r["url"] for r in rows} == {"http://a.com", "http://c.com"}


def test_get_hifi_instances_returns_empty_when_no_rows(db):
    assert db.get_hifi_instances() == []


def test_get_hifi_instances_tiebreaks_on_id(db):
    """Same priority → ordered by insertion order (autoincrement id)."""
    _seed(db, instances=[
        ("http://first.com", 0, 1),
        ("http://second.com", 0, 1),
        ("http://third.com", 0, 1),
    ])
    rows = db.get_hifi_instances()
    assert [r["url"] for r in rows] == ["http://first.com", "http://second.com", "http://third.com"]


# ── get_all_hifi_instances ────────────────────────────────────────────────


def test_get_all_hifi_instances_returns_all_including_disabled(db):
    _seed(db, instances=[
        ("http://a.com", 0, 1),
        ("http://b.com", 1, 0),
    ])
    rows = db.get_all_hifi_instances()
    assert {r["url"] for r in rows} == {"http://a.com", "http://b.com"}


def test_get_all_hifi_instances_ordered_by_priority(db):
    _seed(db, instances=[
        ("http://c.com", 20, 0),
        ("http://a.com", 0, 1),
        ("http://b.com", 10, 1),
    ])
    rows = db.get_all_hifi_instances()
    assert [r["url"] for r in rows] == ["http://a.com", "http://b.com", "http://c.com"]


def test_get_all_hifi_instances_returns_empty_when_no_rows(db):
    assert db.get_all_hifi_instances() == []


# ── add_hifi_instance ─────────────────────────────────────────────────────


def test_add_hifi_instance_returns_true_on_insert(db):
    assert db.add_hifi_instance("http://new.com", priority=3) is True
    rows = db.get_all_hifi_instances()
    assert len(rows) == 1
    assert rows[0]["url"] == "http://new.com"
    assert rows[0]["priority"] == 3
    assert rows[0]["enabled"] == 1


def test_add_hifi_instance_returns_false_on_duplicate(db):
    _seed(db, instances=[("http://dup.com", 0, 1)])
    # INSERT OR IGNORE — should not raise, but return False (rowcount == 0)
    assert db.add_hifi_instance("http://dup.com", priority=5) is False
    rows = db.get_all_hifi_instances()
    assert len(rows) == 1


def test_add_hifi_instance_default_priority(db):
    db.add_hifi_instance("http://x.com")
    row = db.get_all_hifi_instances()[0]
    assert row["priority"] == 0


# ── remove_hifi_instance ──────────────────────────────────────────────────


def test_remove_hifi_instance_returns_true_on_delete(db):
    _seed(db, instances=[("http://go.com", 0, 1)])
    assert db.remove_hifi_instance("http://go.com") is True
    assert db.get_all_hifi_instances() == []


def test_remove_hifi_instance_returns_false_when_not_found(db):
    assert db.remove_hifi_instance("http://missing.com") is False


def test_remove_hifi_instance_only_removes_matching_url(db):
    _seed(db, instances=[
        ("http://keep.com", 0, 1),
        ("http://delete.com", 1, 1),
    ])
    db.remove_hifi_instance("http://delete.com")
    rows = db.get_all_hifi_instances()
    assert len(rows) == 1
    assert rows[0]["url"] == "http://keep.com"


# ── toggle_hifi_instance ──────────────────────────────────────────────────


def test_toggle_hifi_instance_disable(db):
    _seed(db, instances=[("http://x.com", 0, 1)])
    assert db.toggle_hifi_instance("http://x.com", enabled=False) is True
    row = db.get_all_hifi_instances()[0]
    assert row["enabled"] == 0


def test_toggle_hifi_instance_enable(db):
    _seed(db, instances=[("http://x.com", 0, 0)])
    assert db.toggle_hifi_instance("http://x.com", enabled=True) is True
    row = db.get_all_hifi_instances()[0]
    assert row["enabled"] == 1


def test_toggle_hifi_instance_returns_false_when_not_found(db):
    assert db.toggle_hifi_instance("http://missing.com", enabled=True) is False


def test_toggle_hifi_instance_noop_when_already_set(db):
    """Toggling to the same value should still return True (row matched)."""
    _seed(db, instances=[("http://x.com", 0, 1)])
    # SQLite rowcount for UPDATE is 1 even if value didn't change
    assert db.toggle_hifi_instance("http://x.com", enabled=True) is True


# ── reorder_hifi_instances ────────────────────────────────────────────────


def test_reorder_hifi_instances_updates_priorities(db):
    _seed(db, instances=[
        ("http://a.com", 0, 1),
        ("http://b.com", 1, 1),
        ("http://c.com", 2, 1),
    ])
    db.reorder_hifi_instances(["http://c.com", "http://a.com", "http://b.com"])
    rows = db.get_all_hifi_instances()
    by_url = {r["url"]: r["priority"] for r in rows}
    assert by_url == {"http://c.com": 0, "http://a.com": 1, "http://b.com": 2}


def test_reorder_hifi_instances_returns_true_on_empty_list(db):
    assert db.reorder_hifi_instances([]) is True


def test_reorder_hifi_instances_returns_false_with_unknown_urls(db):
    """Reorder should fail when any URL doesn't exist."""
    _seed(db, instances=[("http://a.com", 0, 1)])
    assert db.reorder_hifi_instances(["http://a.com", "http://phantom.com"]) is False


# ── seed_hifi_instances ───────────────────────────────────────────────────


def test_seed_hifi_instances_inserts_when_empty(db):
    db.seed_hifi_instances(["http://a.com", "http://b.com"])
    rows = db.get_all_hifi_instances()
    assert len(rows) == 2
    by_url = {r["url"]: r["priority"] for r in rows}
    assert by_url == {"http://a.com": 0, "http://b.com": 1}


def test_seed_hifi_instances_does_nothing_when_table_has_rows(db):
    _seed(db, instances=[("http://existing.com", 0, 1)])
    db.seed_hifi_instances(["http://new.com"])
    rows = db.get_all_hifi_instances()
    assert len(rows) == 1
    assert rows[0]["url"] == "http://existing.com"


def test_seed_hifi_instances_does_not_duplicate_on_reseed(db):
    db.seed_hifi_instances(["http://a.com"])
    db.seed_hifi_instances(["http://a.com"])
    rows = db.get_all_hifi_instances()
    assert len(rows) == 1


# ── lazy-create recovery (issue #503) ────────────────────────────────────
# When the bulk ``_initialize_database`` rolls back due to a brittle
# migration step, ``hifi_instances`` would normally be missing on the
# user's DB. The CRUD methods now ensure the table exists immediately
# before operating, so the operation succeeds rather than throwing
# "no such table: hifi_instances".


def _db_without_hifi_table():
    """Returns a MusicDatabase with NO hifi_instances table — simulates
    the broken-init state where ``_initialize_database`` rolled back."""
    conn = sqlite3.connect(":memory:")
    conn.row_factory = sqlite3.Row

    class _NoTableDB(MusicDatabase):
        def __init__(self):
            self._conn = conn

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

    return _NoTableDB()


def _hifi_table_exists(db):
    cur = db._conn.cursor()
    cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='hifi_instances'")
    return cur.fetchone() is not None


def test_add_hifi_instance_creates_table_when_missing():
    """Pin issue #503 fix: add_hifi_instance must lazily create the
    table when init failed to land it. Pre-fix this raised
    ``no such table: hifi_instances`` and the user couldn't add any
    HiFi instances at all."""
    db = _db_without_hifi_table()
    assert not _hifi_table_exists(db)

    result = db.add_hifi_instance("http://recovery.com", priority=0)

    assert result is True
    assert _hifi_table_exists(db)
    rows = db.get_all_hifi_instances()
    assert len(rows) == 1
    assert rows[0]["url"] == "http://recovery.com"


def test_get_hifi_instances_creates_table_when_missing():
    """Empty result instead of OperationalError — read methods
    self-heal too."""
    db = _db_without_hifi_table()
    assert not _hifi_table_exists(db)

    rows = db.get_hifi_instances()

    assert rows == []
    assert _hifi_table_exists(db)


def test_get_all_hifi_instances_creates_table_when_missing():
    db = _db_without_hifi_table()
    rows = db.get_all_hifi_instances()
    assert rows == []
    assert _hifi_table_exists(db)


def test_remove_hifi_instance_creates_table_when_missing():
    """Removing from a missing table is a no-op (returns False) —
    doesn't crash."""
    db = _db_without_hifi_table()
    result = db.remove_hifi_instance("http://x.com")
    assert result is False
    assert _hifi_table_exists(db)


def test_toggle_hifi_instance_creates_table_when_missing():
    db = _db_without_hifi_table()
    result = db.toggle_hifi_instance("http://x.com", enabled=True)
    assert result is False  # Nothing to toggle
    assert _hifi_table_exists(db)


def test_reorder_hifi_instances_creates_table_when_missing():
    """Reorder with missing entries returns False (the existing
    contract — caller-supplied URLs not present)."""
    db = _db_without_hifi_table()
    result = db.reorder_hifi_instances(["http://x.com"])
    assert result is False  # No matching rows
    assert _hifi_table_exists(db)


def test_seed_hifi_instances_creates_table_when_missing():
    """Seeding into a missing table works — table is created, then
    defaults inserted."""
    db = _db_without_hifi_table()
    db.seed_hifi_instances(["http://default-a.com", "http://default-b.com"])
    rows = db.get_all_hifi_instances()
    assert len(rows) == 2
    assert _hifi_table_exists(db)
