import pytest
from unittest.mock import AsyncMock, MagicMock
import asyncio

from api.v1.schemas.search import SearchResult, SuggestResponse
from services.search_service import SearchService


def _make_search_result(
    type: str,
    title: str,
    score: int = 0,
    musicbrainz_id: str = "",
    artist: str | None = None,
    year: int | None = None,
    disambiguation: str | None = None,
) -> SearchResult:
    return SearchResult(
        type=type,
        title=title,
        musicbrainz_id=musicbrainz_id or f"mbid-{title.lower().replace(' ', '-')}",
        score=score,
        artist=artist,
        year=year,
        disambiguation=disambiguation,
        in_library=False,
        requested=False,
    )


def _make_preferences(secondary_types: list[str] | None = None) -> MagicMock:
    prefs = MagicMock()
    prefs.secondary_types = secondary_types or []
    return prefs


def _make_service(
    grouped: dict[str, list[SearchResult]] | None = None,
    library_mbids: set[str] | None = None,
    queue_items: list | None = None,
    mb_error: Exception | None = None,
    lidarr_library_error: Exception | None = None,
    lidarr_queue_error: Exception | None = None,
) -> SearchService:
    mb_repo = MagicMock()
    if mb_error:
        mb_repo.search_grouped = AsyncMock(side_effect=mb_error)
    else:
        mb_repo.search_grouped = AsyncMock(return_value=grouped or {"artists": [], "albums": []})

    lidarr_repo = MagicMock()
    if lidarr_library_error:
        lidarr_repo.get_library_mbids = AsyncMock(side_effect=lidarr_library_error)
    else:
        lidarr_repo.get_library_mbids = AsyncMock(return_value=library_mbids or set())

    if lidarr_queue_error:
        lidarr_repo.get_queue = AsyncMock(side_effect=lidarr_queue_error)
    else:
        lidarr_repo.get_queue = AsyncMock(return_value=queue_items or [])

    coverart_repo = MagicMock()
    preferences_service = MagicMock()
    preferences_service.get_preferences.return_value = _make_preferences()

    return SearchService(
        mb_repo=mb_repo,
        lidarr_repo=lidarr_repo,
        coverart_repo=coverart_repo,
        preferences_service=preferences_service,
    )


@pytest.mark.asyncio
async def test_suggest_returns_suggest_response():
    artists = [_make_search_result("artist", "Muse", score=90)]
    albums = [_make_search_result("album", "Origin of Symmetry", score=85, artist="Muse")]
    svc = _make_service(grouped={"artists": artists, "albums": albums})

    result = await svc.suggest(query="muse", limit=5)

    assert isinstance(result, SuggestResponse)
    assert len(result.results) == 2
    assert result.results[0].title == "Muse"
    assert result.results[1].title == "Origin of Symmetry"


@pytest.mark.asyncio
async def test_suggest_score_interleaving():
    artists = [
        _make_search_result("artist", "Artist A", score=90),
        _make_search_result("artist", "Artist B", score=80),
    ]
    albums = [
        _make_search_result("album", "Album X", score=95, artist="X"),
        _make_search_result("album", "Album Y", score=85, artist="Y"),
    ]
    svc = _make_service(grouped={"artists": artists, "albums": albums})

    result = await svc.suggest(query="test", limit=5)

    assert len(result.results) == 4
    assert result.results[0].title == "Album X"
    assert result.results[0].score == 95
    assert result.results[1].title == "Artist A"
    assert result.results[1].score == 90
    assert result.results[2].title == "Album Y"
    assert result.results[2].score == 85
    assert result.results[3].title == "Artist B"
    assert result.results[3].score == 80


@pytest.mark.asyncio
async def test_suggest_equal_score_artist_before_album():
    artists = [_make_search_result("artist", "Bee", score=80)]
    albums = [_make_search_result("album", "Ant", score=80, artist="Someone")]
    svc = _make_service(grouped={"artists": artists, "albums": albums})

    result = await svc.suggest(query="test", limit=5)

    assert len(result.results) == 2
    assert result.results[0].type == "artist"
    assert result.results[0].title == "Bee"
    assert result.results[1].type == "album"
    assert result.results[1].title == "Ant"


@pytest.mark.asyncio
async def test_suggest_alphabetical_tiebreak_within_same_type():
    artists = [
        _make_search_result("artist", "Zebra", score=80),
        _make_search_result("artist", "Alpha", score=80),
    ]
    svc = _make_service(grouped={"artists": artists, "albums": []})

    result = await svc.suggest(query="test", limit=5)

    assert len(result.results) == 2
    assert result.results[0].title == "Alpha"
    assert result.results[1].title == "Zebra"


@pytest.mark.asyncio
async def test_suggest_truncates_to_limit():
    artists = [
        _make_search_result("artist", f"Artist {i}", score=100 - i)
        for i in range(3)
    ]
    albums = [
        _make_search_result("album", f"Album {i}", score=99 - i, artist="X")
        for i in range(3)
    ]
    svc = _make_service(grouped={"artists": artists, "albums": albums})

    result = await svc.suggest(query="test", limit=4)

    assert len(result.results) == 4


@pytest.mark.asyncio
async def test_suggest_lidarr_failure_returns_default_flags():
    artists = [_make_search_result("artist", "Muse", score=90)]
    albums = [
        _make_search_result("album", "Absolution", score=85, artist="Muse",
                            musicbrainz_id="album-1"),
    ]
    svc = _make_service(
        grouped={"artists": artists, "albums": albums},
        lidarr_library_error=Exception("Lidarr unavailable"),
        lidarr_queue_error=Exception("Lidarr unavailable"),
    )

    result = await svc.suggest(query="muse", limit=5)

    assert len(result.results) == 2
    for r in result.results:
        assert r.in_library is False
        assert r.requested is False


@pytest.mark.asyncio
async def test_suggest_musicbrainz_failure_returns_empty():
    svc = _make_service(mb_error=Exception("MusicBrainz down"))

    result = await svc.suggest(query="muse", limit=5)

    assert isinstance(result, SuggestResponse)
    assert len(result.results) == 0


@pytest.mark.asyncio
async def test_suggest_query_normalization():
    artists = [_make_search_result("artist", "Muse", score=90)]
    svc = _make_service(grouped={"artists": artists, "albums": []})

    result = await svc.suggest(query="  muse  ", limit=5)

    assert len(result.results) == 1
    assert result.results[0].title == "Muse"
    svc._mb_repo.search_grouped.assert_called_once()
    call_args = svc._mb_repo.search_grouped.call_args
    assert call_args[0][0] == "muse"


@pytest.mark.asyncio
async def test_suggest_in_library_flag():
    albums = [
        _make_search_result("album", "Absolution", score=85, artist="Muse",
                            musicbrainz_id="album-lib-1"),
    ]
    svc = _make_service(
        grouped={"artists": [], "albums": albums},
        library_mbids={"album-lib-1"},
    )

    result = await svc.suggest(query="absolution", limit=5)

    assert len(result.results) == 1
    assert result.results[0].in_library is True
    assert result.results[0].requested is False


@pytest.mark.asyncio
async def test_suggest_requested_flag():
    albums = [
        _make_search_result("album", "Absolution", score=85, artist="Muse",
                            musicbrainz_id="album-q-1"),
    ]
    queue_item = MagicMock()
    queue_item.musicbrainz_id = "album-q-1"
    svc = _make_service(
        grouped={"artists": [], "albums": albums},
        queue_items=[queue_item],
    )

    result = await svc.suggest(query="absolution", limit=5)

    assert len(result.results) == 1
    assert result.results[0].in_library is False
    assert result.results[0].requested is True


@pytest.mark.asyncio
async def test_suggest_whitespace_only_query_returns_empty():
    """Whitespace-padded query that becomes too short after strip returns empty."""
    svc = _make_service(grouped={"artists": [], "albums": []})

    result = await svc.suggest(query="  a  ", limit=5)

    assert isinstance(result, SuggestResponse)
    assert len(result.results) == 0
    svc._mb_repo.search_grouped.assert_not_called()


@pytest.mark.asyncio
async def test_suggest_single_char_after_strip_returns_empty():
    """Single-char query after stripping returns empty without calling MusicBrainz."""
    svc = _make_service(grouped={"artists": [], "albums": []})

    result = await svc.suggest(query="x", limit=5)

    assert isinstance(result, SuggestResponse)
    assert len(result.results) == 0
    svc._mb_repo.search_grouped.assert_not_called()


@pytest.mark.asyncio
async def test_suggest_case_insensitive_alphabetical_tiebreak():
    """Alphabetical tiebreak is case-insensitive: 'alpha' before 'Bravo'."""
    artists = [
        _make_search_result("artist", "Bravo", score=80),
        _make_search_result("artist", "alpha", score=80),
    ]
    svc = _make_service(grouped={"artists": artists, "albums": []})

    result = await svc.suggest(query="test", limit=5)

    assert len(result.results) == 2
    assert result.results[0].title == "alpha"
    assert result.results[1].title == "Bravo"


@pytest.mark.asyncio
async def test_suggest_deduplication_single_mb_call():
    """Concurrent suggest calls with same normalized query produce only one MusicBrainz call."""
    artists = [_make_search_result("artist", "Muse", score=90)]

    call_event = asyncio.Event()
    call_count = 0

    async def slow_search_grouped(*args, **kwargs):
        nonlocal call_count
        call_count += 1
        await call_event.wait()
        return {"artists": artists, "albums": []}

    mb_repo = MagicMock()
    mb_repo.search_grouped = slow_search_grouped

    lidarr_repo = MagicMock()
    lidarr_repo.get_library_mbids = AsyncMock(return_value=set())
    lidarr_repo.get_queue = AsyncMock(return_value=[])

    coverart_repo = MagicMock()
    preferences_service = MagicMock()
    preferences_service.get_preferences.return_value = _make_preferences()

    svc = SearchService(
        mb_repo=mb_repo,
        lidarr_repo=lidarr_repo,
        coverart_repo=coverart_repo,
        preferences_service=preferences_service,
    )

    task1 = asyncio.create_task(svc.suggest(query="muse", limit=5))
    task2 = asyncio.create_task(svc.suggest(query="muse", limit=5))
    await asyncio.sleep(0.05)
    call_event.set()

    r1, r2 = await asyncio.gather(task1, task2)

    assert call_count == 1
    assert len(r1.results) == 1
    assert len(r2.results) == 1
