import pytest
from unittest.mock import AsyncMock, patch
from io import BytesIO
from pathlib import Path

from fastapi import FastAPI
from fastapi.testclient import TestClient

from api.v1.routes.playlists import router as playlists_router
from core.dependencies import get_playlist_service, get_jellyfin_library_service, get_local_files_service, get_navidrome_library_service
from core.exceptions import PlaylistNotFoundError, InvalidPlaylistDataError, ResourceNotFoundError, ValidationError
from core.exception_handlers import resource_not_found_handler, validation_error_handler, general_exception_handler
from repositories.playlist_repository import PlaylistRecord, PlaylistSummaryRecord, PlaylistTrackRecord


def _playlist(id="p-1", name="Test", cover_image_path=None) -> PlaylistRecord:
    return PlaylistRecord(
        id=id, name=name, cover_image_path=cover_image_path,
        created_at="2025-01-01T00:00:00+00:00",
        updated_at="2025-01-01T00:00:00+00:00",
    )


def _summary(id="p-1", name="Test") -> PlaylistSummaryRecord:
    return PlaylistSummaryRecord(
        id=id, name=name, track_count=3, total_duration=600,
        cover_urls=["http://example.com/cover.jpg"],
        cover_image_path=None,
        created_at="2025-01-01T00:00:00+00:00",
        updated_at="2025-01-01T00:00:00+00:00",
    )


def _track(id="t-1", playlist_id="p-1", position=0) -> PlaylistTrackRecord:
    return PlaylistTrackRecord(
        id=id, playlist_id=playlist_id, position=position,
        track_name="Song", artist_name="Artist", album_name="Album",
        album_id=None, artist_id=None, track_source_id=None, cover_url="http://img/1",
        source_type="local", available_sources=None, format=None,
        track_number=1, disc_number=2, duration=180,
        created_at="2025-01-01T00:00:00+00:00",
    )


@pytest.fixture
def mock_playlist_service():
    mock = AsyncMock()
    mock.create_playlist.return_value = _playlist()
    mock.get_playlist.return_value = _playlist()
    mock.get_all_playlists.return_value = [_summary()]
    mock.get_playlist_with_tracks.return_value = (_playlist(), [_track()])
    mock.update_playlist.return_value = _playlist()
    mock.update_playlist_with_detail.return_value = (_playlist(), [_track()])
    mock.delete_playlist.return_value = None
    mock.add_tracks.return_value = [_track()]
    mock.remove_track.return_value = None
    mock.reorder_track.return_value = 2
    mock.update_track_source.return_value = _track()
    mock.get_tracks.return_value = [_track()]
    mock.upload_cover.return_value = "/api/v1/playlists/p-1/cover"
    mock.get_cover_path.return_value = None
    mock.remove_cover.return_value = None
    return mock


@pytest.fixture
def mock_jf_service():
    return AsyncMock()


@pytest.fixture
def mock_local_service():
    return AsyncMock()


@pytest.fixture
def mock_nd_service():
    return AsyncMock()


@pytest.fixture
def client(mock_playlist_service, mock_jf_service, mock_local_service, mock_nd_service):
    app = FastAPI()
    app.include_router(playlists_router)
    app.dependency_overrides[get_playlist_service] = lambda: mock_playlist_service
    app.dependency_overrides[get_jellyfin_library_service] = lambda: mock_jf_service
    app.dependency_overrides[get_local_files_service] = lambda: mock_local_service
    app.dependency_overrides[get_navidrome_library_service] = lambda: mock_nd_service
    app.add_exception_handler(ResourceNotFoundError, resource_not_found_handler)
    app.add_exception_handler(ValidationError, validation_error_handler)
    app.add_exception_handler(Exception, general_exception_handler)
    return TestClient(app)


class TestListPlaylists:
    def test_success(self, client):
        resp = client.get("/playlists")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data["playlists"]) == 1
        assert data["playlists"][0]["name"] == "Test"
        assert data["playlists"][0]["track_count"] == 3

    def test_empty(self, client, mock_playlist_service):
        mock_playlist_service.get_all_playlists.return_value = []
        resp = client.get("/playlists")
        assert resp.status_code == 200
        assert len(resp.json()["playlists"]) == 0


class TestCreatePlaylist:
    def test_success(self, client):
        resp = client.post(
            "/playlists",
            content=b'{"name": "My Playlist"}',
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 201
        data = resp.json()
        assert data["id"] == "p-1"
        assert data["tracks"] == []
        assert "cover_image_path" not in data

    def test_validation_error(self, client, mock_playlist_service):
        mock_playlist_service.create_playlist.side_effect = InvalidPlaylistDataError("empty name")
        resp = client.post(
            "/playlists",
            content=b'{"name": ""}',
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 400


class TestGetPlaylist:
    def test_success(self, client):
        resp = client.get("/playlists/p-1")
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == "p-1"
        assert len(data["tracks"]) == 1
        assert data["track_count"] == 1
        assert data["tracks"][0]["disc_number"] == 2
        assert "cover_image_path" not in data

    def test_not_found(self, client, mock_playlist_service):
        mock_playlist_service.get_playlist_with_tracks.side_effect = PlaylistNotFoundError("nope")
        resp = client.get("/playlists/nonexistent")
        assert resp.status_code == 404


class TestUpdatePlaylist:
    def test_success(self, client):
        resp = client.put(
            "/playlists/p-1",
            content=b'{"name": "Renamed"}',
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 200
        assert resp.json()["id"] == "p-1"
        assert "cover_image_path" not in resp.json()

    def test_not_found(self, client, mock_playlist_service):
        mock_playlist_service.update_playlist_with_detail.side_effect = PlaylistNotFoundError("nope")
        resp = client.put(
            "/playlists/nonexistent",
            content=b'{"name": "X"}',
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 404


class TestDeletePlaylist:
    def test_success(self, client):
        resp = client.delete("/playlists/p-1")
        assert resp.status_code == 200
        assert resp.json()["status"] == "ok"

    def test_not_found(self, client, mock_playlist_service):
        mock_playlist_service.delete_playlist.side_effect = PlaylistNotFoundError("nope")
        resp = client.delete("/playlists/nonexistent")
        assert resp.status_code == 404


class TestAddTracks:
    def test_success(self, client):
        body = {
            "tracks": [
                {"track_name": "S", "artist_name": "A", "album_name": "AL", "disc_number": 2},
            ],
        }
        resp = client.post(
            "/playlists/p-1/tracks",
            content=__import__("json").dumps(body).encode(),
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 201
        assert len(resp.json()["tracks"]) == 1
        assert resp.json()["tracks"][0]["disc_number"] == 2

    def test_empty_tracks(self, client, mock_playlist_service):
        mock_playlist_service.add_tracks.side_effect = InvalidPlaylistDataError("empty")
        resp = client.post(
            "/playlists/p-1/tracks",
            content=b'{"tracks": []}',
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 400

    def test_playlist_not_found(self, client, mock_playlist_service):
        mock_playlist_service.add_tracks.side_effect = PlaylistNotFoundError("nope")
        body = {"tracks": [{"track_name": "S", "artist_name": "A", "album_name": "AL"}]}
        resp = client.post(
            "/playlists/nonexistent/tracks",
            content=__import__("json").dumps(body).encode(),
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 404


class TestRemoveTrack:
    def test_success(self, client):
        resp = client.delete("/playlists/p-1/tracks/t-1")
        assert resp.status_code == 200
        assert resp.json()["status"] == "ok"

    def test_not_found(self, client, mock_playlist_service):
        mock_playlist_service.remove_track.side_effect = PlaylistNotFoundError("nope")
        resp = client.delete("/playlists/p-1/tracks/nonexistent")
        assert resp.status_code == 404


class TestReorderTrack:
    def test_success(self, client):
        resp = client.patch(
            "/playlists/p-1/tracks/reorder",
            content=b'{"track_id": "t-1", "new_position": 2}',
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "ok"
        assert data["actual_position"] == 2

    def test_invalid_position(self, client, mock_playlist_service):
        mock_playlist_service.reorder_track.side_effect = InvalidPlaylistDataError("neg")
        resp = client.patch(
            "/playlists/p-1/tracks/reorder",
            content=b'{"track_id": "t-1", "new_position": -1}',
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 400


class TestUpdateTrack:
    def test_success(self, client, mock_playlist_service):
        updated = _track()
        updated.source_type = "youtube"
        mock_playlist_service.update_track_source.return_value = updated
        resp = client.patch(
            "/playlists/p-1/tracks/t-1",
            content=b'{"source_type": "youtube"}',
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 200
        assert resp.json()["source_type"] == "youtube"

    def test_not_found(self, client, mock_playlist_service):
        mock_playlist_service.update_track_source.side_effect = PlaylistNotFoundError("nope")
        resp = client.patch(
            "/playlists/p-1/tracks/nonexistent",
            content=b'{"source_type": "youtube"}',
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 404


class TestUploadCover:
    def test_success(self, client):
        resp = client.post(
            "/playlists/p-1/cover",
            files={"cover_image": ("cover.png", b"PNG_DATA", "image/png")},
        )
        assert resp.status_code == 200
        assert resp.json()["cover_url"] == "/api/v1/playlists/p-1/cover"

    def test_invalid_type(self, client, mock_playlist_service):
        mock_playlist_service.upload_cover.side_effect = InvalidPlaylistDataError("bad type")
        resp = client.post(
            "/playlists/p-1/cover",
            files={"cover_image": ("doc.pdf", b"data", "application/pdf")},
        )
        assert resp.status_code == 400


class TestGetCover:
    def test_no_cover(self, client):
        resp = client.get("/playlists/p-1/cover")
        assert resp.status_code == 404

    def test_with_cover(self, client, mock_playlist_service, tmp_path):
        cover_file = tmp_path / "cover.png"
        cover_file.write_bytes(b"\x89PNG\r\n\x1a\n")
        mock_playlist_service.get_cover_path.return_value = cover_file
        resp = client.get("/playlists/p-1/cover")
        assert resp.status_code == 200
        assert resp.headers["content-type"] == "image/png"
        assert "max-age=3600" in resp.headers.get("cache-control", "")


class TestRemoveCover:
    def test_success(self, client):
        resp = client.delete("/playlists/p-1/cover")
        assert resp.status_code == 200
        assert resp.json()["status"] == "ok"


class TestCheckTrackMembership:
    def test_success(self, client, mock_playlist_service):
        mock_playlist_service.check_track_membership.return_value = {
            "p-1": [0, 1],
            "p-2": [1],
        }
        resp = client.post(
            "/playlists/check-tracks",
            json={
                "tracks": [
                    {"track_name": "Song A", "artist_name": "Artist", "album_name": "Album"},
                    {"track_name": "Song B", "artist_name": "Artist2", "album_name": "Album2"},
                ]
            },
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["membership"]["p-1"] == [0, 1]
        assert data["membership"]["p-2"] == [1]

    def test_empty_tracks(self, client, mock_playlist_service):
        mock_playlist_service.check_track_membership.return_value = {}
        resp = client.post(
            "/playlists/check-tracks",
            json={"tracks": []},
        )
        assert resp.status_code == 200
        assert resp.json()["membership"] == {}


class TestResolveSources:
    def test_success(self, client, mock_playlist_service):
        mock_playlist_service.resolve_track_sources.return_value = {
            "t-1": ["jellyfin", "local"],
            "t-2": ["jellyfin"],
        }
        resp = client.post("/playlists/p-1/resolve-sources")
        assert resp.status_code == 200
        data = resp.json()
        assert data["sources"]["t-1"] == ["jellyfin", "local"]
        assert data["sources"]["t-2"] == ["jellyfin"]

    def test_empty_sources(self, client, mock_playlist_service):
        mock_playlist_service.resolve_track_sources.return_value = {}
        resp = client.post("/playlists/p-1/resolve-sources")
        assert resp.status_code == 200
        assert resp.json()["sources"] == {}

    def test_not_found(self, client, mock_playlist_service):
        mock_playlist_service.resolve_track_sources.side_effect = PlaylistNotFoundError("nope")
        resp = client.post("/playlists/p-1/resolve-sources")
        assert resp.status_code == 404


class TestUpdateTrackSourceResolution:
    def test_returns_updated_track_source_id(self, client, mock_playlist_service):
        updated = _track()
        updated.source_type = "jellyfin"
        updated.track_source_id = "jf-resolved-id"
        updated.available_sources = ["jellyfin", "local"]
        mock_playlist_service.update_track_source.return_value = updated
        resp = client.patch(
            "/playlists/p-1/tracks/t-1",
            content=b'{"source_type": "jellyfin"}',
            headers={"Content-Type": "application/json"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["source_type"] == "jellyfin"
        assert data["track_source_id"] == "jf-resolved-id"
        assert data["available_sources"] == ["jellyfin", "local"]
