"""Unit tests for downloads endpoint pagination.

Fix 4.1: `GET /api/v1/downloads` previously returned every task in the
in-memory `download_tasks` dict on every call. With many downloads this
produces a huge payload. The endpoint now supports `limit`, `offset`,
and `status` query params and includes a `total` count.
"""

import sys
import types
from unittest.mock import patch

import pytest


# api/__init__.py eagerly imports flask_limiter. Stub before import.
def _install_flask_limiter_stub():
    if "flask_limiter" in sys.modules:
        return
    stub = types.ModuleType("flask_limiter")

    class _Limiter:
        def __init__(self, *args, **kwargs):
            pass

        def limit(self, *args, **kwargs):
            def decorator(target):
                return target
            return decorator

        def init_app(self, app):
            pass

    stub.Limiter = _Limiter
    sys.modules["flask_limiter"] = stub

    util_stub = types.ModuleType("flask_limiter.util")
    util_stub.get_remote_address = lambda: "127.0.0.1"
    sys.modules["flask_limiter.util"] = util_stub


_install_flask_limiter_stub()

from flask import Flask, Blueprint  # noqa: E402

from api import downloads as downloads_mod  # noqa: E402
import core.runtime_state as runtime_state  # noqa: E402


def _make_task(status="downloading", when=None):
    return {
        "status": status,
        "track_name": f"Track {when}",
        "artist_name": "Artist",
        "album_name": "Album",
        "username": "user",
        "filename": "file.mp3",
        "progress": 0,
        "size": 1000,
        "status_change_time": when or "2026-01-01T00:00:00",
    }


def _make_app_with_tasks(tasks_dict):
    """Create a minimal Flask app with the downloads blueprint mounted and
    the shared import runtime state populated with the given download_tasks dict."""
    original_tasks = dict(runtime_state.download_tasks)
    runtime_state.download_tasks.clear()
    runtime_state.download_tasks.update(tasks_dict)

    # Bypass API key auth for tests.
    def _passthrough(f):
        return f

    app = Flask(__name__)
    bp = Blueprint("v1", __name__, url_prefix="/api/v1")

    with patch.object(downloads_mod, "require_api_key", _passthrough):
        # downloads.register_routes was already imported with the real
        # decorator bound, but register_routes runs fresh decorators at
        # call time against the passed blueprint.
        downloads_mod.register_routes(bp)

    app.register_blueprint(bp)
    app._original_download_tasks = original_tasks
    return app


@pytest.fixture
def client():
    tasks = {
        f"task-{i:03d}": _make_task(
            status="downloading" if i % 2 == 0 else "queued",
            when=f"2026-01-{i+1:02d}T00:00:00",
        )
        for i in range(25)
    }
    app = _make_app_with_tasks(tasks)
    try:
        with app.test_client() as c:
            yield c
    finally:
        runtime_state.download_tasks.clear()
        runtime_state.download_tasks.update(app._original_download_tasks)


def test_default_limit_applied(client):
    # 25 tasks, default limit is 100 -> all fit in one page.
    resp = client.get("/api/v1/downloads")
    assert resp.status_code == 200
    data = resp.get_json()["data"]
    assert data["total"] == 25
    assert data["limit"] == 100
    assert data["offset"] == 0
    assert len(data["downloads"]) == 25


def test_limit_and_offset_slice_correctly(client):
    resp = client.get("/api/v1/downloads?limit=5&offset=0")
    data = resp.get_json()["data"]
    assert len(data["downloads"]) == 5
    assert data["total"] == 25

    resp2 = client.get("/api/v1/downloads?limit=5&offset=20")
    data2 = resp2.get_json()["data"]
    assert len(data2["downloads"]) == 5
    # Pages should not overlap.
    page1_ids = {t["id"] for t in data["downloads"]}
    page5_ids = {t["id"] for t in data2["downloads"]}
    assert page1_ids.isdisjoint(page5_ids)


def test_status_filter_single(client):
    resp = client.get("/api/v1/downloads?status=downloading&limit=100")
    data = resp.get_json()["data"]
    # 13 even-indexed tasks (0,2,...,24)
    assert data["total"] == 13
    for t in data["downloads"]:
        assert t["status"] == "downloading"


def test_status_filter_multiple_comma_separated(client):
    resp = client.get("/api/v1/downloads?status=downloading,queued&limit=100")
    data = resp.get_json()["data"]
    assert data["total"] == 25


def test_status_filter_no_match_returns_empty(client):
    resp = client.get("/api/v1/downloads?status=nonexistent_status")
    data = resp.get_json()["data"]
    assert data["total"] == 0
    assert data["downloads"] == []


def test_limit_is_clamped_to_max(client):
    resp = client.get("/api/v1/downloads?limit=99999")
    data = resp.get_json()["data"]
    assert data["limit"] == 500


def test_negative_offset_is_normalized(client):
    resp = client.get("/api/v1/downloads?offset=-5")
    data = resp.get_json()["data"]
    assert data["offset"] == 0


def test_invalid_limit_falls_back_to_default(client):
    resp = client.get("/api/v1/downloads?limit=not_a_number")
    data = resp.get_json()["data"]
    assert data["limit"] == 100


def test_tasks_sorted_newest_first(client):
    resp = client.get("/api/v1/downloads?limit=3&offset=0")
    data = resp.get_json()["data"]
    times = [t["status_change_time"] for t in data["downloads"]]
    # Most recent (2026-01-25) should come first.
    assert times == sorted(times, reverse=True)
