"""Tests for peer-review fixes: route collision, byYear contract, audio-only filter,
favorites/expanded error handling, timed lyrics, and Plex accountID removal."""
from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from api.v1.routes.navidrome_library import router as navidrome_router
from api.v1.routes.jellyfin_library import router as jellyfin_router
from api.v1.schemas.navidrome import (
    NavidromeArtistIndexResponse,
    NavidromeArtistIndexEntry,
    NavidromeArtistSummary,
    NavidromeAlbumSummary,
    NavidromeAlbumPage,
)
from api.v1.schemas.jellyfin import JellyfinFavoritesExpanded
from core.dependencies import get_navidrome_library_service, get_jellyfin_library_service
from core.exceptions import ExternalServiceError
from repositories.navidrome_models import SubsonicLyrics, SubsonicLyricLine


def _navidrome_app(mock_service) -> TestClient:
    """Router already has prefix=/navidrome, so routes are at /navidrome/..."""
    app = FastAPI()
    app.include_router(navidrome_router)
    app.dependency_overrides[get_navidrome_library_service] = lambda: mock_service
    return TestClient(app)


def _jellyfin_app(mock_service) -> TestClient:
    """Router already has prefix=/jellyfin, so routes are at /jellyfin/..."""
    app = FastAPI()
    app.include_router(jellyfin_router)
    app.dependency_overrides[get_jellyfin_library_service] = lambda: mock_service
    return TestClient(app)


class TestNavidromeArtistIndexRouteOrder:
    """Verify /artists/index resolves to the index handler, not the artist-detail handler."""

    def test_artists_index_resolves_correctly(self):
        mock = MagicMock()
        mock.get_artists_index = AsyncMock(return_value=NavidromeArtistIndexResponse(
            index=[
                NavidromeArtistIndexEntry(
                    name="A",
                    artists=[NavidromeArtistSummary(navidrome_id="ar1", name="ABBA")],
                ),
            ]
        ))
        client = _navidrome_app(mock)
        resp = client.get("/navidrome/artists/index")
        assert resp.status_code == 200
        assert "index" in resp.json()
        mock.get_artists_index.assert_awaited_once()
        mock.get_artist_detail = AsyncMock()
        mock.get_artist_detail.assert_not_awaited()

    def test_artist_detail_still_works(self):
        mock = MagicMock()
        mock.get_artist_detail = AsyncMock(return_value={
            "artist": {"navidrome_id": "real-id", "name": "Artist"},
            "albums": [],
        })
        client = _navidrome_app(mock)
        resp = client.get("/navidrome/artists/real-id")
        assert resp.status_code == 200
        mock.get_artist_detail.assert_awaited_once_with("real-id")


class TestNavidromeByYearSort:
    """Verify that year sort sends fromYear/toYear to the service."""

    def test_year_sort_asc_sends_year_params(self):
        mock = MagicMock()
        mock.get_albums = AsyncMock(return_value=[])
        mock.get_stats = AsyncMock(side_effect=ExternalServiceError("unavailable"))
        client = _navidrome_app(mock)
        resp = client.get("/navidrome/albums", params={"sort_by": "year", "sort_order": ""})
        assert resp.status_code == 200
        call_kwargs = mock.get_albums.call_args.kwargs
        assert call_kwargs.get("from_year") == 0
        assert call_kwargs.get("to_year") == 9999

    def test_year_sort_desc_sends_reversed_year_params(self):
        mock = MagicMock()
        mock.get_albums = AsyncMock(return_value=[])
        mock.get_stats = AsyncMock(side_effect=ExternalServiceError("unavailable"))
        client = _navidrome_app(mock)
        resp = client.get("/navidrome/albums", params={"sort_by": "year", "sort_order": "desc"})
        assert resp.status_code == 200
        call_kwargs = mock.get_albums.call_args.kwargs
        assert call_kwargs.get("from_year") == 9999
        assert call_kwargs.get("to_year") == 0

    def test_name_sort_does_not_send_year_params(self):
        mock = MagicMock()
        mock.get_albums = AsyncMock(return_value=[])
        mock.get_stats = AsyncMock(side_effect=ExternalServiceError("unavailable"))
        client = _navidrome_app(mock)
        resp = client.get("/navidrome/albums", params={"sort_by": "name"})
        assert resp.status_code == 200
        call_kwargs = mock.get_albums.call_args.kwargs
        assert "from_year" not in call_kwargs
        assert "to_year" not in call_kwargs


class TestJellyfinPlaylistAudioFilter:
    """Verify that non-audio items are filtered out of playlist responses."""

    @pytest.mark.asyncio
    async def test_get_playlist_items_filters_non_audio(self):
        from repositories.jellyfin_models import JellyfinItem
        from repositories.jellyfin_repository import JellyfinRepository

        repo = MagicMock(spec=JellyfinRepository)
        repo._configured = True
        repo._user_id = "u1"
        repo._cache = MagicMock()
        repo._cache.get = AsyncMock(return_value=None)
        repo._cache.set = AsyncMock()

        mixed_items = {
            "Items": [
                {"Id": "a1", "Name": "Song 1", "Type": "Audio", "RunTimeTicks": 1800000000},
                {"Id": "v1", "Name": "Video", "Type": "Video", "RunTimeTicks": 9000000000},
                {"Id": "a2", "Name": "Song 2", "Type": "Audio", "RunTimeTicks": 2000000000},
            ]
        }
        repo._get = AsyncMock(return_value=mixed_items)

        result = await JellyfinRepository.get_playlist_items(repo, "pl-1")
        assert len(result) == 2
        assert all(item.type == "Audio" for item in result)
        assert result[0].name == "Song 1"
        assert result[1].name == "Song 2"


class TestJellyfinFavoritesExpandedErrorHandling:
    """Verify that unexpected errors in favorites/expanded return a proper HTTP error."""

    def test_unexpected_error_returns_500(self):
        mock = MagicMock()
        mock.get_favorites_expanded = AsyncMock(side_effect=RuntimeError("unexpected"))
        client = _jellyfin_app(mock)
        resp = client.get("/jellyfin/favorites/expanded")
        assert resp.status_code == 500

    def test_external_service_error_returns_502(self):
        mock = MagicMock()
        mock.get_favorites_expanded = AsyncMock(side_effect=ExternalServiceError("Jellyfin down"))
        client = _jellyfin_app(mock)
        resp = client.get("/jellyfin/favorites/expanded")
        assert resp.status_code == 502

    def test_success_returns_200(self):
        mock = MagicMock()
        mock.get_favorites_expanded = AsyncMock(return_value=JellyfinFavoritesExpanded(albums=[], artists=[]))
        client = _jellyfin_app(mock)
        resp = client.get("/jellyfin/favorites/expanded")
        assert resp.status_code == 200


class TestNavidromeLyricsTimedPreservation:
    """Verify that timed lyrics lines are preserved through the backend contract."""

    @pytest.mark.asyncio
    async def test_synced_lyrics_preserve_timing(self):
        from services.navidrome_library_service import NavidromeLibraryService

        lyrics = SubsonicLyrics(
            value="Line one\nLine two",
            lines=[
                SubsonicLyricLine(value="Line one", start=0),
                SubsonicLyricLine(value="Line two", start=5000),
            ],
            is_synced=True,
        )
        repo = MagicMock()
        repo.get_lyrics_by_song_id = AsyncMock(return_value=lyrics)
        repo.get_lyrics = AsyncMock(return_value=None)
        repo.get_albums = AsyncMock(return_value=[])
        repo.get_recently_played = AsyncMock(return_value=[])
        repo.get_recently_added = AsyncMock(return_value=[])
        repo.get_starred_albums = AsyncMock(return_value=[])
        repo.get_starred_artists = AsyncMock(return_value=[])
        repo.get_starred_songs = AsyncMock(return_value=[])
        repo.get_genres = AsyncMock(return_value=[])
        repo.get_album_count = AsyncMock(return_value=0)
        repo.get_track_count = AsyncMock(return_value=0)
        repo.get_artist_count = AsyncMock(return_value=0)
        prefs = MagicMock()
        svc = NavidromeLibraryService(navidrome_repo=repo, preferences_service=prefs)

        result = await svc.get_lyrics("song-1")
        assert result is not None
        assert result.is_synced is True
        assert len(result.lines) == 2
        assert result.lines[0].text == "Line one"
        assert result.lines[0].start_seconds == pytest.approx(0.0)
        assert result.lines[1].text == "Line two"
        assert result.lines[1].start_seconds == pytest.approx(5.0)
        assert "Line one" in result.text

    @pytest.mark.asyncio
    async def test_unsynced_lyrics_have_no_timing(self):
        from services.navidrome_library_service import NavidromeLibraryService

        lyrics = SubsonicLyrics(
            value="Plain lyrics\nSecond line",
            lines=[
                SubsonicLyricLine(value="Plain lyrics", start=None),
                SubsonicLyricLine(value="Second line", start=None),
            ],
            is_synced=False,
        )
        repo = MagicMock()
        repo.get_lyrics_by_song_id = AsyncMock(return_value=lyrics)
        repo.get_lyrics = AsyncMock(return_value=None)
        repo.get_albums = AsyncMock(return_value=[])
        repo.get_recently_played = AsyncMock(return_value=[])
        repo.get_recently_added = AsyncMock(return_value=[])
        repo.get_starred_albums = AsyncMock(return_value=[])
        repo.get_starred_artists = AsyncMock(return_value=[])
        repo.get_starred_songs = AsyncMock(return_value=[])
        repo.get_genres = AsyncMock(return_value=[])
        repo.get_album_count = AsyncMock(return_value=0)
        repo.get_track_count = AsyncMock(return_value=0)
        repo.get_artist_count = AsyncMock(return_value=0)
        prefs = MagicMock()
        svc = NavidromeLibraryService(navidrome_repo=repo, preferences_service=prefs)

        result = await svc.get_lyrics("song-1")
        assert result is not None
        assert result.is_synced is False
        assert all(l.start_seconds is None for l in result.lines)


class TestPlexHistoryNoHardcodedAccount:
    """Verify that the Plex history endpoint does not hardcode accountID."""

    @pytest.mark.asyncio
    async def test_history_params_exclude_account_id(self):
        from repositories.plex_repository import PlexRepository

        repo = MagicMock(spec=PlexRepository)
        repo._configured = True
        repo._cache = MagicMock()
        repo._cache.get = AsyncMock(return_value=None)
        repo._cache.set = AsyncMock()
        repo._request = AsyncMock(return_value={
            "MediaContainer": {
                "size": 0,
                "totalSize": 0,
                "Metadata": [],
            }
        })

        await PlexRepository.get_listening_history(repo)
        call_args = repo._request.call_args
        params = call_args[1].get("params") or call_args[0][1] if len(call_args[0]) > 1 else call_args.kwargs.get("params")
        assert "accountID" not in params
