"""Direct-stream preparation tests; no media file is ever created."""

from __future__ import annotations

import asyncio
from types import SimpleNamespace
from typing import Any

import pytest

from app.database.models import Video
from app.errors import (
    ContentTooLargeError,
    DomainNotAllowedError,
    DownloadCancelledError,
    InvalidMediaError,
    MediaNotFoundError,
    NetworkError,
    RateLimitedError,
)
from app.services.downloader import Downloader
from app.services.http_client import MediaStreamInfo
from tests.helpers import MP4_HEADER


class RecordingGuard:
    def __init__(self, error: Exception | None = None) -> None:
        self.urls: list[str] = []
        self.error = error

    async def validate(self, url: str, *, resolve_dns: bool = True) -> Any:
        self.urls.append(url)
        if self.error is not None:
            raise self.error
        return SimpleNamespace(url=url)


class FakeHttp:
    def __init__(self, outcomes: list[Any] | None = None) -> None:
        self.outcomes = list(outcomes or [])
        self.calls: list[dict[str, Any]] = []

    async def probe_media(
        self, url: str, *, max_bytes: int, referer: str | None = None
    ) -> MediaStreamInfo:
        self.calls.append({"url": url, "max_bytes": max_bytes, "referer": referer})
        if self.outcomes:
            outcome = self.outcomes.pop(0)
            if isinstance(outcome, BaseException):
                raise outcome
            return outcome
        return MediaStreamInfo(
            requested_url=url,
            final_url=url,
            status=200,
            content_type="video/mp4",
            total_size=len(MP4_HEADER) + 64,
            mime_type="video/mp4",
            extension=".mp4",
            container="mp4",
        )


def _video(
    *,
    media_url: str | None = "https://example.com/video.mp4",
    page_url: str = "https://example.com/watch/1",
    title: str = "Example video",
    file_size: int | None = None,
) -> Video:
    return Video.create(
        job_id="a" * 32,
        source_url=page_url,
        page_url=page_url,
        title=title,
        caption=title,
        media_type="video/mp4",
        media_url=media_url,
        file_size=file_size,
    )


def _downloader(
    settings, http: FakeHttp | None = None, guard: RecordingGuard | None = None
) -> tuple[Downloader, FakeHttp, RecordingGuard]:
    guard = guard or RecordingGuard()
    http = http or FakeHttp()
    return Downloader(settings, guard, http), http, guard


@pytest.mark.asyncio
async def test_prepare_returns_stream_metadata_without_writing_files(settings, tmp_path) -> None:
    downloader, http, guard = _downloader(settings)

    result = await downloader.prepare(_video(), asyncio.Event())

    assert result.method == "stream"
    assert result.size == len(MP4_HEADER) + 64
    assert result.mime_type == "video/mp4"
    assert result.extension == ".mp4"
    assert guard.urls == ["https://example.com/video.mp4"]
    assert http.calls[0]["max_bytes"] == min(
        settings.max_file_size_bytes, settings.telegram_upload_limit_bytes
    )
    assert http.calls[0]["referer"] == "https://example.com/watch/1"
    assert not (tmp_path / "downloads").exists()


@pytest.mark.asyncio
async def test_prepare_rejects_missing_media_url(settings) -> None:
    downloader, http, guard = _downloader(settings)
    with pytest.raises(MediaNotFoundError, match="direct media URL"):
        await downloader.prepare(_video(media_url=None), asyncio.Event())
    assert http.calls == []
    assert guard.urls == []


@pytest.mark.asyncio
async def test_prepare_rejects_playlist_urls(settings) -> None:
    downloader, http, _ = _downloader(settings)
    with pytest.raises(MediaNotFoundError, match="playlist"):
        await downloader.prepare(
            _video(media_url="https://example.com/v.m3u8?token=1"), asyncio.Event()
        )
    assert http.calls == []


@pytest.mark.asyncio
async def test_prepare_propagates_guard_rejection(settings) -> None:
    guard = RecordingGuard(DomainNotAllowedError("no"))
    downloader, http, _ = _downloader(settings, guard=guard)
    with pytest.raises(DomainNotAllowedError):
        await downloader.prepare(_video(), asyncio.Event())
    assert http.calls == []


@pytest.mark.asyncio
async def test_prepare_rejects_html_and_executable_payloads(settings) -> None:
    for payload in (InvalidMediaError("signature"), InvalidMediaError("executable")):
        downloader, _, _ = _downloader(settings, http=FakeHttp([payload]))
        with pytest.raises(InvalidMediaError):
            await downloader.prepare(_video(), asyncio.Event())


@pytest.mark.asyncio
async def test_prepare_cancel_before_start(settings) -> None:
    downloader, http, _ = _downloader(settings)
    cancel_event = asyncio.Event()
    cancel_event.set()
    with pytest.raises(DownloadCancelledError):
        await downloader.prepare(_video(), cancel_event)
    assert http.calls == []


@pytest.mark.asyncio
async def test_prepare_retries_network_failure_then_succeeds(
    settings, monkeypatch: pytest.MonkeyPatch
) -> None:
    settings.retry_count = 1
    http = FakeHttp([NetworkError("boom")])
    downloader, _, _ = _downloader(settings, http=http)

    async def no_wait(seconds: float, cancel_event: asyncio.Event) -> None:
        return None

    monkeypatch.setattr(Downloader, "_sleep", staticmethod(no_wait))
    result = await downloader.prepare(_video(), asyncio.Event())
    assert len(http.calls) == 2
    assert result.size is not None


@pytest.mark.asyncio
async def test_prepare_retries_rate_limit(settings, monkeypatch: pytest.MonkeyPatch) -> None:
    settings.retry_count = 1
    http = FakeHttp([RateLimitedError(0)])
    downloader, _, _ = _downloader(settings, http=http)

    async def no_wait(seconds: float, cancel_event: asyncio.Event) -> None:
        return None

    monkeypatch.setattr(Downloader, "_sleep", staticmethod(no_wait))
    await downloader.prepare(_video(), asyncio.Event())
    assert len(http.calls) == 2


@pytest.mark.asyncio
async def test_prepare_stops_after_retry_exhaustion(
    settings, monkeypatch: pytest.MonkeyPatch
) -> None:
    settings.retry_count = 1
    http = FakeHttp([NetworkError("first"), NetworkError("second")])
    downloader, _, _ = _downloader(settings, http=http)

    async def no_wait(seconds: float, cancel_event: asyncio.Event) -> None:
        return None

    monkeypatch.setattr(Downloader, "_sleep", staticmethod(no_wait))
    with pytest.raises(NetworkError, match="second"):
        await downloader.prepare(_video(), asyncio.Event())
    assert len(http.calls) == 2


@pytest.mark.asyncio
async def test_prepare_rejects_declared_size_above_limit_before_probe(settings) -> None:
    settings.max_file_size_mb = 1
    settings.telegram_max_upload_mb = 1
    downloader, http, _ = _downloader(settings)
    with pytest.raises(ContentTooLargeError):
        await downloader.prepare(_video(file_size=2 * 1024 * 1024), asyncio.Event())
    assert http.calls == []


@pytest.mark.asyncio
async def test_prepare_propagates_probe_size_rejection(settings) -> None:
    http = FakeHttp([ContentTooLargeError()])
    downloader, _, _ = _downloader(settings, http=http)
    with pytest.raises(ContentTooLargeError):
        await downloader.prepare(_video(), asyncio.Event())
