"""Unit tests for DownloadService — DB-free, in-memory fakes."""

import datetime as dt
import uuid
from pathlib import Path

import pytest
from app.application.download_service import DownloadService
from app.domain.entities import Artist, Track
from app.domain.entities.download import DownloadJob
from app.domain.sources import DownloadResult

pytestmark = pytest.mark.asyncio


class FakeArtistRepo:
    async def get_or_create(self, name: str) -> Artist:
        now = dt.datetime.now(dt.UTC)
        return Artist(
            id=uuid.uuid4(),
            name=name,
            source=None,
            source_id=None,
            created_at=now,
            updated_at=now,
        )


class FakeTrackRepo:
    def __init__(self) -> None:
        self.by_source: dict[tuple[str, str], Track] = {}
        self.added: list[Track] = []

    async def get_by_source(self, source: str, source_id: str) -> Track | None:
        return self.by_source.get((source, source_id))

    async def add(self, **kw: object) -> Track:
        now = dt.datetime.now(dt.UTC)
        track = Track(
            id=kw["id"],  # type: ignore[arg-type]
            title=str(kw["title"]),
            artist_id=kw["artist_id"],  # type: ignore[arg-type]
            album_id=None,
            storage_uri=str(kw["storage_uri"]),
            file_format=str(kw["file_format"]),
            file_size=int(kw["file_size"]),  # type: ignore[call-overload]
            source=str(kw["source"]),
            source_id=str(kw["source_id"]),
            duration_seconds=None,
            genre=None,
            year=None,
            track_number=None,
            metadata_status=str(kw["metadata_status"]),
            metadata_error=None,
            enriched_at=None,
            availability="local",
            created_at=now,
            updated_at=now,
        )
        self.by_source[(track.source, track.source_id)] = track
        self.added.append(track)
        return track


class FakeStorage:
    def __init__(self) -> None:
        self.saved: dict[str, Path] = {}
        self.deleted: list[str] = []

    async def save_file(self, key: str, src_path: Path) -> int:
        self.saved[key] = src_path
        return 1

    async def delete(self, key: str) -> None:
        self.deleted.append(key)


class FakeJobRepo:
    def __init__(self) -> None:
        self.jobs: dict[uuid.UUID, DownloadJob] = {}
        self.active: dict[tuple[str, str], DownloadJob] = {}

    def _make(self, **kw: object) -> DownloadJob:
        now = dt.datetime.now(dt.UTC)
        return DownloadJob(
            id=uuid.uuid4(),
            source=str(kw["source"]),
            source_id=kw.get("source_id"),  # type: ignore[arg-type]
            query=kw.get("query"),  # type: ignore[arg-type]
            requested_by=kw.get("requested_by"),  # type: ignore[arg-type]
            status="queued",
            progress=0.0,
            error_message=None,
            retry_count=0,
            track_id=None,
            created_at=now,
            updated_at=now,
        )

    async def add(self, **kw: object) -> DownloadJob:
        job = self._make(**kw)
        self.jobs[job.id] = job
        return job

    async def get_by_id(self, job_id: uuid.UUID) -> DownloadJob | None:
        return self.jobs.get(job_id)

    async def get_active_for_source(self, source: str, source_id: str) -> DownloadJob | None:
        return self.active.get((source, source_id))

    async def set_status(self, job_id: uuid.UUID, **kw: object) -> None: ...

    async def delete(self, job_id: uuid.UUID) -> None:
        self.jobs.pop(job_id, None)


def _service(
    *, jobs: FakeJobRepo, tracks: FakeTrackRepo, storage: FakeStorage, enqueued: list[uuid.UUID]
) -> DownloadService:
    async def enqueue_download(job_id: uuid.UUID) -> None:
        enqueued.append(job_id)

    return DownloadService(
        jobs=jobs,  # type: ignore[arg-type]
        tracks=tracks,  # type: ignore[arg-type]
        artists=FakeArtistRepo(),  # type: ignore[arg-type]
        storage=storage,  # type: ignore[arg-type]
        enqueue_download=enqueue_download,
    )


def _track(source: str, source_id: str) -> Track:
    now = dt.datetime.now(dt.UTC)
    return Track(
        id=uuid.uuid4(),
        title="t",
        artist_id=uuid.uuid4(),
        album_id=None,
        storage_uri="k",
        file_format="mp3",
        file_size=1,
        source=source,
        source_id=source_id,
        duration_seconds=None,
        genre=None,
        year=None,
        track_number=None,
        metadata_status="pending",
        metadata_error=None,
        enriched_at=None,
        availability="local",
        created_at=now,
        updated_at=now,
    )


async def test_request_dedups_against_library() -> None:
    jobs, tracks, storage, enq = FakeJobRepo(), FakeTrackRepo(), FakeStorage(), []
    tracks.by_source[("youtube", "abc")] = _track("youtube", "abc")
    svc = _service(jobs=jobs, tracks=tracks, storage=storage, enqueued=enq)

    result = await svc.request(source="youtube", source_id="abc", query=None, requested_by=None)

    assert result.already_in_library is True
    assert result.track_id is not None
    assert result.job is None
    assert enq == []  # nothing enqueued


async def test_request_returns_existing_active_job() -> None:
    jobs, tracks, storage, enq = FakeJobRepo(), FakeTrackRepo(), FakeStorage(), []
    existing = await jobs.add(source="youtube", source_id="abc", query=None, requested_by=None)
    jobs.active[("youtube", "abc")] = existing
    svc = _service(jobs=jobs, tracks=tracks, storage=storage, enqueued=enq)

    result = await svc.request(source="youtube", source_id="abc", query=None, requested_by=None)

    assert result.already_in_library is False
    assert result.job is not None
    assert result.job.id == existing.id
    assert enq == []  # not re-enqueued


async def test_request_creates_and_enqueues_new_job() -> None:
    jobs, tracks, storage, enq = FakeJobRepo(), FakeTrackRepo(), FakeStorage(), []
    svc = _service(jobs=jobs, tracks=tracks, storage=storage, enqueued=enq)

    result = await svc.request(
        source="youtube", source_id="abc", query="bohemian", requested_by=None
    )

    assert result.already_in_library is False
    assert result.job is not None
    assert enq == [result.job.id]


async def test_store_result_imports_and_cleans_temp(tmp_path: Path) -> None:
    jobs, tracks, storage, enq = FakeJobRepo(), FakeTrackRepo(), FakeStorage(), []
    svc = _service(jobs=jobs, tracks=tracks, storage=storage, enqueued=enq)

    audio = tmp_path / "abc.webm"
    audio.write_bytes(b"audio" * 20)
    result = DownloadResult(
        source_id="abc",
        path=audio,
        file_format="m4a",
        file_size=100,
        bitrate=160,
        suggested_title="Bohemian Rhapsody",
    )

    track_id = await svc.store_result(source="youtube", result=result, requested_by=None)

    assert len(tracks.added) == 1
    stored = tracks.added[0]
    assert stored.id == track_id
    assert stored.source == "youtube"
    assert stored.source_id == "abc"
    assert stored.metadata_status == "pending"
    assert stored.title == "Bohemian Rhapsody"
    assert len(storage.saved) == 1
    assert not audio.exists()  # temp file removed
