"""Tests for core/library/manual_library_match.py and DB methods."""

from __future__ import annotations

import json
from unittest.mock import MagicMock, patch

import pytest

from core.library import manual_library_match as mlm
from database.music_database import MusicDatabase


@pytest.fixture
def db(tmp_path):
    return MusicDatabase(str(tmp_path / "music.db"))


# ---------------------------------------------------------------------------
# DB-layer tests
# ---------------------------------------------------------------------------

def test_save_and_get_roundtrip(db):
    ok = db.save_manual_library_match(1, "spotify", "track-abc", 42,
                                       source_title="HUMBLE.", source_artist="Kendrick Lamar",
                                       source_album="DAMN.", server_source="")
    assert ok is True
    row = db.get_manual_library_match(1, "spotify", "track-abc")
    assert row is not None
    assert row["library_track_id"] == 42
    assert row["source_title"] == "HUMBLE."
    assert row["source_artist"] == "Kendrick Lamar"


def test_save_upserts_existing(db):
    db.save_manual_library_match(1, "spotify", "track-abc", 42)
    db.save_manual_library_match(1, "spotify", "track-abc", 99, source_title="Updated")
    row = db.get_manual_library_match(1, "spotify", "track-abc")
    assert row["library_track_id"] == 99
    assert row["source_title"] == "Updated"


def test_delete_by_id(db):
    db.save_manual_library_match(1, "spotify", "track-abc", 42)
    row = db.get_manual_library_match(1, "spotify", "track-abc")
    assert row is not None
    ok = db.delete_manual_library_match(row["id"], 1)
    assert ok is True
    assert db.get_manual_library_match(1, "spotify", "track-abc") is None


def test_list_matches_scoped_to_profile(db):
    db.save_manual_library_match(1, "spotify", "t1", 10)
    db.save_manual_library_match(1, "spotify", "t2", 20)
    rows = db.list_manual_library_matches(1)
    assert len(rows) == 2
    # Ordered by updated_at DESC — most recent first
    ids = {r["library_track_id"] for r in rows}
    assert ids == {10, 20}


def test_profile_isolation(db):
    db.save_manual_library_match(1, "spotify", "track-abc", 10)
    db.save_manual_library_match(2, "spotify", "track-abc", 20)
    assert db.get_manual_library_match(1, "spotify", "track-abc")["library_track_id"] == 10
    assert db.get_manual_library_match(2, "spotify", "track-abc")["library_track_id"] == 20
    assert len(db.list_manual_library_matches(1)) == 1
    assert len(db.list_manual_library_matches(2)) == 1


def test_server_source_isolation(db):
    db.save_manual_library_match(1, "spotify", "track-abc", 10, server_source="plex")
    db.save_manual_library_match(1, "spotify", "track-abc", 20, server_source="jellyfin")
    assert db.get_manual_library_match(1, "spotify", "track-abc", "plex")["library_track_id"] == 10
    assert db.get_manual_library_match(1, "spotify", "track-abc", "jellyfin")["library_track_id"] == 20


def test_get_returns_none_when_absent(db):
    assert db.get_manual_library_match(1, "spotify", "nonexistent") is None


def test_get_match_for_track_falls_back_across_source_labels(db):
    db.save_manual_library_match(
        1,
        "mirrored",
        "track-abc",
        42,
        source_title="Coffee Break",
        source_artist="Zeds Dead",
    )

    row = mlm.get_match_for_track(
        db,
        1,
        {
            "id": "track-abc",
            "name": "Coffee Break",
            "artists": [{"name": "Zeds Dead"}],
            "provider": "wishlist",
        },
        default_source="wishlist",
    )

    assert row is not None
    assert row["library_track_id"] == 42


def test_get_match_for_track_falls_back_to_source_title_artist(db):
    db.save_manual_library_match(
        1,
        "mirrored",
        "old-id",
        42,
        source_title="Coffee Break",
        source_artist="Zeds Dead",
    )

    row = mlm.get_match_for_track(
        db,
        1,
        {
            "id": "new-id",
            "name": "Coffee Break",
            "artists": [{"name": "Zeds Dead"}],
            "provider": "musicbrainz",
        },
        default_source="wishlist",
    )

    assert row is not None
    assert row["library_track_id"] == 42


def test_add_to_wishlist_skips_manual_matched_track(db):
    db.save_manual_library_match(1, "spotify", "track-abc", 42)

    ok = db.add_to_wishlist(
        track_data={
            "id": "track-abc",
            "name": "HUMBLE.",
            "artists": [{"name": "Kendrick Lamar"}],
            "album": {"name": "DAMN."},
            "provider": "spotify",
        },
        failure_reason="Download failed",
        profile_id=1,
    )

    assert ok is True
    assert db.get_wishlist_tracks(profile_id=1) == []


def test_add_to_wishlist_skips_manual_match_saved_from_mirrored_source(db):
    db.save_manual_library_match(
        1,
        "mirrored",
        "track-abc",
        42,
        source_title="Coffee Break",
        source_artist="Zeds Dead",
    )

    ok = db.add_to_wishlist(
        track_data={
            "id": "track-abc",
            "name": "Coffee Break",
            "artists": [{"name": "Zeds Dead"}],
            "album": {"name": "Coffee Break"},
            "provider": "wishlist",
        },
        failure_reason="Download failed",
        profile_id=1,
    )

    assert ok is True
    assert db.get_wishlist_tracks(profile_id=1) == []


def test_get_match_returns_none_when_db_lacks_manual_match_method():
    class _MinimalDB:
        pass

    assert mlm.get_match(_MinimalDB(), 1, "spotify", "track-abc") is None


# ---------------------------------------------------------------------------
# Service-layer tests
# ---------------------------------------------------------------------------

def test_service_save_and_get(db):
    mlm.save_match(db, 1, "spotify", "t1", 42, source_title="Song A")
    row = mlm.get_match(db, 1, "spotify", "t1")
    assert row is not None
    assert row["library_track_id"] == 42


def test_service_delete(db):
    mlm.save_match(db, 1, "spotify", "t1", 42)
    row = mlm.get_match(db, 1, "spotify", "t1")
    mlm.delete_match(db, row["id"], 1)
    assert mlm.get_match(db, 1, "spotify", "t1") is None


def test_service_list_enriches(db):
    mlm.save_match(db, 1, "spotify", "t1", 42, source_title="Song A", source_artist="Artist X")
    with patch.object(db, "api_get_tracks_by_ids", return_value=[{"title": "Song A", "artist_name": "Artist X", "album_title": "Album Z", "file_path": "/music/a.flac", "bitrate": 320}]):
        matches = mlm.list_matches(db, 1)
    assert len(matches) == 1
    assert matches[0]["library_title"] == "Song A"


def test_search_library_candidates(db):
    with patch.object(db, "api_search_tracks", return_value=[{"id": 1, "title": "HUMBLE.", "artist_name": "Kendrick Lamar"}]) as mock_search:
        results = mlm.search_library_candidates(db, "HUMBLE")
    # searches by title AND artist, deduped
    assert mock_search.call_count == 2
    assert len(results) == 1
    assert results[0]["title"] == "HUMBLE."


def test_search_source_candidates_empty_query(db):
    results = mlm.search_source_candidates(db, "", 1)
    assert results == []


# ---------------------------------------------------------------------------
# Integration: wishlist processing
# ---------------------------------------------------------------------------

def test_wishlist_skips_manual_matched_track():
    """Manual match causes track to be removed from wishlist without fuzzy check."""
    track = {
        "name": "HUMBLE.",
        "artists": [{"name": "Kendrick Lamar"}],
        "spotify_track_id": "spotify-track-123",
    }

    mock_wishlist_svc = MagicMock()
    mock_wishlist_svc.get_wishlist_tracks_for_download.return_value = [track]
    mock_wishlist_svc.mark_track_download_result.return_value = True

    mock_profiles_db = MagicMock()
    mock_profiles_db.get_all_profiles.return_value = [{"id": 1}]

    mock_music_db = MagicMock()
    mock_music_db.check_track_exists = MagicMock()

    with patch("core.library.manual_library_match.get_match_for_track", return_value={"id": 1, "library_track_id": 42}):
        from core.wishlist.processing import remove_tracks_already_in_library
        removed = remove_tracks_already_in_library(
            mock_wishlist_svc,
            mock_profiles_db,
            mock_music_db,
            active_server="plex",
        )

    assert removed == 1
    mock_wishlist_svc.mark_track_download_result.assert_called_once_with("spotify-track-123", success=True)
    mock_music_db.check_track_exists.assert_not_called()


def test_wishlist_falls_through_when_no_match():
    """No manual match → fuzzy path runs normally."""
    track = {
        "name": "HUMBLE.",
        "artists": [{"name": "Kendrick Lamar"}],
        "spotify_track_id": "spotify-track-123",
    }

    mock_wishlist_svc = MagicMock()
    mock_wishlist_svc.get_wishlist_tracks_for_download.return_value = [track]
    mock_wishlist_svc.mark_track_download_result.return_value = False

    mock_profiles_db = MagicMock()
    mock_profiles_db.get_all_profiles.return_value = [{"id": 1}]

    mock_music_db = MagicMock()
    mock_music_db.check_track_exists.return_value = (None, 0.0)

    with patch("core.library.manual_library_match.get_match_for_track", return_value=None):
        from core.wishlist.processing import remove_tracks_already_in_library
        removed = remove_tracks_already_in_library(
            mock_wishlist_svc,
            mock_profiles_db,
            mock_music_db,
            active_server="plex",
        )

    assert removed == 0
    mock_music_db.check_track_exists.assert_called()


# ---------------------------------------------------------------------------
# Integration: downloads/master analysis loop
# ---------------------------------------------------------------------------

def test_master_analysis_marks_found():
    """Manual match causes the real analysis loop to mark track found=True."""
    from core.downloads.master import MasterDeps, run_full_missing_tracks_process
    from core.runtime_state import download_batches, tasks_lock
    import threading

    batch_id = "test-batch-real-123"
    track_data = {"name": "HUMBLE.", "artists": [], "id": "spotify-track-abc"}

    with tasks_lock:
        download_batches[batch_id] = {
            "phase": "analysis",
            "analysis_total": 1,
            "analysis_processed": 0,
            "force_download_all": False,
            "is_album_download": False,
            "album_context": None,
            "artist_context": None,
            "profile_id": 1,
            "batch_source": "spotify",
            "wing_it": False,
            "playlist_folder_mode": False,
            "queue": [],
            "active_count": 0,
            "max_concurrent": 1,
            "queue_index": 0,
            "permanently_failed_tracks": [],
            "cancelled_tracks": set(),
            "analysis_results": [],
        }

    mock_db = MagicMock()
    mock_db.check_track_exists.return_value = (None, 0.0)
    mock_db.update_sync_history_completion = MagicMock()

    mock_deps = MagicMock()
    mock_deps.config_manager.get.return_value = False
    mock_deps.config_manager.get_active_media_server.return_value = "plex"
    mock_deps.mb_worker = None
    mock_deps.mb_release_cache = {}
    mock_deps.mb_release_cache_lock = threading.Lock()
    mock_deps.mb_release_detail_cache = {}
    mock_deps.mb_release_detail_cache_lock = threading.Lock()
    mock_deps.normalize_album_cache_key = lambda x: x.lower().strip()
    mock_deps.check_and_remove_track_from_wishlist_by_metadata = MagicMock()
    mock_deps.is_explicit_blocked = MagicMock(return_value=False)
    mock_deps.youtube_playlist_states = {}
    mock_deps.tidal_discovery_states = {}
    mock_deps.deezer_discovery_states = {}
    mock_deps.spotify_public_discovery_states = {}
    mock_deps.missing_download_executor = MagicMock()
    mock_deps.process_failed_tracks_to_wishlist_exact_with_auto_completion = MagicMock()
    mock_deps.source_reuse_logger = MagicMock()
    mock_deps.download_monitor = MagicMock()
    mock_deps.start_next_batch_of_downloads = MagicMock()
    mock_deps.reset_wishlist_auto_processing = MagicMock()

    with patch("core.library.manual_library_match.get_match_for_track", return_value={"id": 1, "library_track_id": 42}), \
         patch("database.music_database.MusicDatabase", return_value=mock_db):
        run_full_missing_tracks_process(batch_id, "playlist-1", [track_data], mock_deps)

    with tasks_lock:
        results = download_batches.get(batch_id, {}).get("analysis_results", [])
        download_batches.pop(batch_id, None)

    assert len(results) == 1
    assert results[0]["found"] is True
    assert results[0]["match_reason"] == "manual_library_match"
    mock_deps.check_and_remove_track_from_wishlist_by_metadata.assert_called_once_with(track_data)


def test_master_analysis_manual_match_wins_over_internal_force_download():
    """Manual match overrides internal force_download_all used by wishlist batches."""
    from core.downloads.master import run_full_missing_tracks_process
    from core.runtime_state import download_batches, tasks_lock
    import threading

    batch_id = "test-batch-force-456"
    track_data = {"name": "HUMBLE.", "artists": [], "id": "spotify-track-abc"}

    with tasks_lock:
        download_batches[batch_id] = {
            "phase": "analysis",
            "analysis_total": 1,
            "analysis_processed": 0,
            "force_download_all": True,  # would normally bypass DB check and queue download
            "ignore_manual_matches": False,
            "is_album_download": False,
            "album_context": None,
            "artist_context": None,
            "profile_id": 1,
            "batch_source": "spotify",
            "wing_it": False,
            "playlist_folder_mode": False,
            "queue": [],
            "active_count": 0,
            "max_concurrent": 1,
            "queue_index": 0,
            "permanently_failed_tracks": [],
            "cancelled_tracks": set(),
            "analysis_results": [],
        }

    mock_db = MagicMock()
    mock_db.check_track_exists.return_value = (None, 0.0)
    mock_db.update_sync_history_completion = MagicMock()

    mock_deps = MagicMock()
    mock_deps.config_manager.get.return_value = False
    mock_deps.config_manager.get_active_media_server.return_value = "plex"
    mock_deps.mb_worker = None
    mock_deps.mb_release_cache = {}
    mock_deps.mb_release_cache_lock = threading.Lock()
    mock_deps.mb_release_detail_cache = {}
    mock_deps.mb_release_detail_cache_lock = threading.Lock()
    mock_deps.normalize_album_cache_key = lambda x: x.lower().strip()
    mock_deps.check_and_remove_track_from_wishlist_by_metadata = MagicMock()
    mock_deps.is_explicit_blocked = MagicMock(return_value=False)
    mock_deps.youtube_playlist_states = {}
    mock_deps.tidal_discovery_states = {}
    mock_deps.deezer_discovery_states = {}
    mock_deps.spotify_public_discovery_states = {}
    mock_deps.missing_download_executor = MagicMock()
    mock_deps.process_failed_tracks_to_wishlist_exact_with_auto_completion = MagicMock()
    mock_deps.source_reuse_logger = MagicMock()
    mock_deps.download_monitor = MagicMock()
    mock_deps.start_next_batch_of_downloads = MagicMock()
    mock_deps.reset_wishlist_auto_processing = MagicMock()

    with patch("core.library.manual_library_match.get_match_for_track", return_value={"id": 1, "library_track_id": 42}), \
         patch("database.music_database.MusicDatabase", return_value=mock_db):
        run_full_missing_tracks_process(batch_id, "playlist-1", [track_data], mock_deps)

    with tasks_lock:
        results = download_batches.get(batch_id, {}).get("analysis_results", [])
        queue = download_batches.get(batch_id, {}).get("queue", [])
        download_batches.pop(batch_id, None)

    assert len(results) == 1
    assert results[0]["found"] is True
    assert results[0]["match_reason"] == "manual_library_match"
    # Track must NOT enter the download queue despite internal force_download_all=True
    assert queue == []
    mock_deps.check_and_remove_track_from_wishlist_by_metadata.assert_called_once_with(track_data)
