"""Tests for the Music Collection Completeness feature.

Covers normalisation, fuzzy matching, scan upserts, deleted-file handling,
completeness statuses, and that manual ignore decisions survive a recompute.

Scanning uses a fake tag reader so we don't need real audio files with embedded
metadata — the scanner's database behaviour is what's under test.
"""

from pathlib import Path

import pytest

from services import db
from services import music_library as ml
from services import text_normalize as tn


@pytest.fixture
def temp_db(tmp_path, monkeypatch):
    monkeypatch.setattr(db, "DB_PATH", tmp_path / "test.db")
    db.init_db()
    yield


def _fake_tags(path: Path) -> dict:
    return {
        "artist": path.parent.parent.name,
        "album": path.parent.name,
        "title": path.stem,
        "track_number": 1,
        "disc_number": 1,
        "year": 1997,
        "artist_mbid": None,
        "album_mbid": None,
        "track_mbid": None,
    }


def _make_track(root: Path, artist: str, album: str, name: str) -> Path:
    folder = root / artist / album
    folder.mkdir(parents=True, exist_ok=True)
    f = folder / name
    f.write_bytes(b"\x00" * 64)
    return f


# ── normalisation ─────────────────────────────────────────────────────────────


def test_normalize_strips_editions_but_keeps_original():
    assert tn.normalize_title("OK Computer (Deluxe Edition)") == tn.normalize_title("OK Computer")
    assert tn.normalize_title("The Bends - 2009 Remaster") == "the bends"
    assert tn.normalize_title("In Rainbows [Remastered]") == "in rainbows"


def test_normalize_artist_drops_leading_the():
    assert tn.normalize_artist("The Beatles") == "beatles"
    assert tn.normalize_artist("AC/DC") == "ac dc"


def test_is_various_artists():
    assert tn.is_various_artists("Various Artists")
    assert tn.is_various_artists("VA")
    assert not tn.is_various_artists("Radiohead")


# ── fuzzy matching / classification ───────────────────────────────────────────


def _local(title, year=1997, mbid=None, _id=1):
    return {"id": _id, "title": title, "title_normalized": tn.normalize_title(title), "year": year, "mbid": mbid}


def test_classify_mbid_match():
    locals_ = [_local("Whatever", mbid="mbid-123")]
    status, conf, reason, lid = tn.classify_release("Different Title", 2000, "mbid-123", locals_)
    assert status == tn.OWNED and conf == 1.0 and lid == 1


def test_classify_title_and_year():
    locals_ = [_local("OK Computer", 1997)]
    status, *_ = tn.classify_release("OK Computer", 1997, None, locals_)
    assert status == tn.OWNED
    # within a year still counts as owned
    status2, *_ = tn.classify_release("OK Computer", 1998, None, locals_)
    assert status2 == tn.OWNED


def test_classify_fuzzy_and_missing():
    locals_ = [_local("OK Computer", 1997)]
    status, conf, *_ = tn.classify_release("OK Komputer", None, None, locals_)
    assert status == tn.PROBABLY_OWNED
    status2, *_ = tn.classify_release("Kid A", 2000, None, locals_)
    assert status2 == tn.MISSING


# ── scan upserts + deleted handling ───────────────────────────────────────────


def test_scan_upserts_and_skips_unchanged(temp_db, tmp_path, monkeypatch):
    monkeypatch.setattr(ml, "_read_tags", _fake_tags)
    root = tmp_path / "music"
    _make_track(root, "Radiohead", "OK Computer", "01 - Airbag.mp3")
    _make_track(root, "Radiohead", "OK Computer", "02 - Paranoid Android.mp3")
    _make_track(root, "Portishead", "Dummy", "01 - Mysterons.flac")

    result = ml.run_scan(root)
    assert result["status"] == "completed"

    with db.connect() as conn:
        artists = conn.execute("SELECT COUNT(*) c FROM library_artists WHERE is_active=1").fetchone()["c"]
        albums = conn.execute("SELECT COUNT(*) c FROM library_albums WHERE is_active=1").fetchone()["c"]
        tracks = conn.execute("SELECT COUNT(*) c FROM library_tracks WHERE is_active=1").fetchone()["c"]
    assert (artists, albums, tracks) == (2, 2, 3)

    # Re-scan unchanged: counts stable, no duplicate rows.
    ml.run_scan(root)
    with db.connect() as conn:
        tracks = conn.execute("SELECT COUNT(*) c FROM library_tracks").fetchone()["c"]
    assert tracks == 3


def test_deleted_file_marked_inactive(temp_db, tmp_path, monkeypatch):
    monkeypatch.setattr(ml, "_read_tags", _fake_tags)
    root = tmp_path / "music"
    f1 = _make_track(root, "Radiohead", "OK Computer", "01 - Airbag.mp3")
    _make_track(root, "Radiohead", "OK Computer", "02 - Paranoid Android.mp3")
    ml.run_scan(root)

    f1.unlink()
    ml.run_scan(root)

    with db.connect() as conn:
        gone = conn.execute("SELECT is_active FROM library_tracks WHERE file_path=?", (str(f1),)).fetchone()
        active_tracks = conn.execute("SELECT COUNT(*) c FROM library_tracks WHERE is_active=1").fetchone()["c"]
        # album still active because one track remains
        album_active = conn.execute("SELECT is_active FROM library_albums LIMIT 1").fetchone()["is_active"]
    assert gone["is_active"] == 0
    assert active_tracks == 1
    assert album_active == 1


def test_deleted_album_deactivates_album_and_artist(temp_db, tmp_path, monkeypatch):
    monkeypatch.setattr(ml, "_read_tags", _fake_tags)
    root = tmp_path / "music"
    f = _make_track(root, "Solo", "Only Album", "01 - Track.mp3")
    ml.run_scan(root)
    f.unlink()
    ml.run_scan(root)
    with db.connect() as conn:
        album_active = conn.execute("SELECT is_active FROM library_albums LIMIT 1").fetchone()["is_active"]
        artist_active = conn.execute("SELECT is_active FROM library_artists LIMIT 1").fetchone()["is_active"]
    assert album_active == 0
    assert artist_active == 0


# ── completeness statuses + manual decisions ──────────────────────────────────


def _seed_artist_with_release(year_local=1997):
    """Insert one artist, one local album (OK Computer), and three external
    release groups (owned, missing, live-excluded). Returns artist id."""
    with db.connect() as conn:
        cur = conn.execute(
            "INSERT INTO library_artists(name, name_normalized, is_active, created_at, updated_at) VALUES('Radiohead','radiohead',1,'','')"
        )
        artist_id = cur.lastrowid
        conn.execute(
            "INSERT INTO library_albums(artist_id, title, title_normalized, year, is_active, created_at, updated_at) VALUES(?,?,?,?,1,'','')",
            (artist_id, "OK Computer", tn.normalize_title("OK Computer"), year_local),
        )
        for mbid, title, yr, prim, sec in [
            ("rg-ok", "OK Computer", 1997, "Album", "[]"),
            ("rg-kida", "Kid A", 2000, "Album", "[]"),
            ("rg-live", "I Might Be Wrong: Live Recordings", 2001, "Album", '["Live"]'),
        ]:
            conn.execute(
                "INSERT INTO external_releases(artist_id, mb_release_group_mbid, title, title_normalized, first_release_year, primary_type, secondary_types, fetched_at) "
                "VALUES(?,?,?,?,?,?,?,'')",
                (artist_id, mbid, title, tn.normalize_title(title), yr, prim, sec),
            )
        ml.recompute_completeness(conn, artist_id)
    return artist_id


def test_completeness_statuses(temp_db):
    artist_id = _seed_artist_with_release()
    with db.connect() as conn:
        rows = {
            r["title"]: r
            for r in conn.execute(
                "SELECT title, status FROM collection_completeness WHERE artist_id=?", (artist_id,)
            ).fetchall()
        }
    assert rows["OK Computer"]["status"] == tn.OWNED
    assert rows["Kid A"]["status"] == tn.MISSING
    # The live album is filtered out — no completeness row created.
    assert "I Might Be Wrong: Live Recordings" not in rows


def test_manual_ignore_survives_recompute(temp_db):
    artist_id = _seed_artist_with_release()
    with db.connect() as conn:
        kid = conn.execute(
            "SELECT id FROM collection_completeness WHERE artist_id=? AND title='Kid A'", (artist_id,)
        ).fetchone()
    ml.set_album_decision(kid["id"], "ignore")

    # A metadata refresh recomputes — the manual decision must persist.
    with db.connect() as conn:
        ml.recompute_completeness(conn, artist_id)
        row = conn.execute("SELECT status, manual_override FROM collection_completeness WHERE id=?", (kid["id"],)).fetchone()
    assert row["status"] == "ignored"
    assert row["manual_override"] == 1


def test_reset_decision_recomputes(temp_db):
    artist_id = _seed_artist_with_release()
    with db.connect() as conn:
        kid = conn.execute(
            "SELECT id FROM collection_completeness WHERE artist_id=? AND title='Kid A'", (artist_id,)
        ).fetchone()
    ml.set_album_decision(kid["id"], "owned")
    ml.set_album_decision(kid["id"], "reset")
    with db.connect() as conn:
        row = conn.execute("SELECT status, manual_override FROM collection_completeness WHERE id=?", (kid["id"],)).fetchone()
    assert row["manual_override"] == 0
    assert row["status"] == tn.MISSING  # Kid A is not in the local library
