"""Shared handler helpers: URL parsing, job selection and safe rendering."""

from __future__ import annotations

from types import SimpleNamespace
from typing import Any

import pytest
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import Message

from app.database.models import Job, JobStatus, Video, VideoStatus
from app.errors import AppError, JobStateError, NetworkError
from app.handlers.common import (
    error_text,
    extract_url,
    first_argument,
    history_text_from_jobs,
    history_text_from_videos,
    job_detail_text,
    resolve_job,
    safe_edit,
    send_job_card,
)
from app.handlers.settings import settings_text
from app.handlers.start import help_text


def _job(source: str = "https://example.com/a") -> Job:
    return Job.create(source, owner_id=1, owner_chat_id=2)


def test_first_argument_and_extract_url() -> None:
    assert first_argument(None) is None
    assert first_argument("") is None
    assert first_argument("  abc def  ") == "abc"
    assert extract_url("see https://example.com/watch?v=1, please") == (
        "https://example.com/watch?v=1"
    )
    assert extract_url("(https://example.com/a)") == "https://example.com/a"
    assert extract_url("not a url") == "not a url"
    assert extract_url("https://example.com/a]") == "https://example.com/a"
    assert extract_url(None or "") == ""


def test_error_text_uses_user_message_for_app_errors() -> None:
    assert error_text(NetworkError()) == NetworkError.default_user_message
    custom = AppError("internal detail", user_message="پیام کاربر")
    assert error_text(custom) == "پیام کاربر"
    assert "جزئیات" in error_text(RuntimeError("secret detail"))
    assert "secret detail" not in error_text(RuntimeError("secret detail"))


@pytest.mark.asyncio
async def test_resolve_job_by_exact_id_prefix_and_latest(repository) -> None:
    job = _job()
    job.id = "a" * 32
    await repository.create_job(job)

    exact = await resolve_job(SimpleNamespace(repo=repository), job.id)
    assert exact.id == job.id

    by_prefix = await resolve_job(SimpleNamespace(repo=repository), "#aaaaaaaa")
    assert by_prefix.id == job.id

    latest = await resolve_job(SimpleNamespace(repo=repository), None)
    assert latest.id == job.id


@pytest.mark.asyncio
async def test_resolve_job_reports_missing_ambiguous_and_empty(repository) -> None:
    ctx = SimpleNamespace(repo=repository)
    with pytest.raises(JobStateError, match="هیچ عملیات فعالی"):
        await resolve_job(ctx, None)

    first = _job()
    first.id = "abcdef" + "0" * 26
    second = _job("https://example.com/b")
    second.id = "abcdfe" + "1" * 26
    await repository.create_job(first)
    await repository.create_job(second)

    with pytest.raises(JobStateError, match="پیدا نشد"):
        await resolve_job(ctx, "ffff")
    with pytest.raises(JobStateError, match="مشترک"):
        await resolve_job(ctx, "abcd")


@pytest.mark.asyncio
async def test_job_detail_text_includes_status_counts(repository) -> None:
    job = _job()
    job.total_videos = 3
    await repository.create_job(job)
    for index, status in enumerate(
        (VideoStatus.UPLOADED, VideoStatus.FAILED, VideoStatus.PENDING)
    ):
        video = Video.create(
            job_id=job.id,
            source_url=f"https://example.com/v{index}",
            page_url="https://example.com",
            title=f"video {index}",
            caption=None,
            media_type="video/mp4",
        )
        video.status = status
        await repository.add_video(video)

    text = await job_detail_text(SimpleNamespace(repo=repository), job)
    assert "وضعیت ویدئوها" in text
    assert "ارسال‌شده" in text
    assert "ناموفق" in text
    assert "در انتظار" in text
    assert job.short_id in text


@pytest.mark.asyncio
async def test_job_detail_text_without_videos(repository) -> None:
    job = _job()
    await repository.create_job(job)
    text = await job_detail_text(SimpleNamespace(repo=repository), job)
    assert "وضعیت ویدئوها" not in text


def test_history_text_from_jobs_escapes_and_handles_empty() -> None:
    assert "هنوز عملیاتی ثبت نشده" in history_text_from_jobs([])
    job = _job("https://example.com/<script>")
    job.status = JobStatus.COMPLETED
    job.total_videos = 2
    job.uploaded_videos = 1
    text = history_text_from_jobs([job])
    assert "&lt;script&gt;" in text
    assert "<script>" not in text
    assert job.short_id in text
    assert "1 از 2" in text


def test_history_text_from_videos_escapes_and_handles_empty() -> None:
    assert "هنوز ویدئویی" in history_text_from_videos([])
    video = Video.create(
        job_id="j",
        source_url="https://example.com/<b>",
        page_url="https://example.com",
        title="<b>nice</b>",
        caption=None,
        media_type="video/mp4",
    )
    video.uploaded_at = "2026-10-25T10:20:30+00:00"
    video.telegram_message_id = 55
    text = history_text_from_videos([video])
    assert "&lt;b&gt;nice&lt;/b&gt;" in text
    assert "2026-10-25 10:20" in text
    assert "55" in text


@pytest.mark.asyncio
async def test_safe_edit_updates_the_existing_message(monkeypatch: pytest.MonkeyPatch) -> None:
    edits: list[dict[str, Any]] = []

    async def edit_text(self: Message, text: str, **kwargs: Any) -> Message:
        edits.append({"text": text, **kwargs})
        return self

    monkeypatch.setattr(Message, "edit_text", edit_text)
    message = Message.model_construct(message_id=1)
    callback = SimpleNamespace(message=message, bot=SimpleNamespace(), from_user=SimpleNamespace(id=1))
    await safe_edit(callback, "<b>text</b>", None)
    assert edits and edits[0]["parse_mode"] == "HTML"
    assert edits[0]["text"] == "<b>text</b>"


@pytest.mark.asyncio
async def test_safe_edit_ignores_not_modified_and_falls_back(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    sent: list[dict[str, Any]] = []

    class FakeBot:
        async def send_message(self, chat_id: int, text: str, **kwargs: Any) -> None:
            sent.append({"chat_id": chat_id, "text": text, **kwargs})

    async def not_modified(self: Message, text: str, **kwargs: Any) -> Message:
        raise TelegramBadRequest(None, "Bad Request: message is not modified")

    monkeypatch.setattr(Message, "edit_text", not_modified)
    message = Message.model_construct(message_id=1)
    callback = SimpleNamespace(
        message=message,
        bot=FakeBot(),
        from_user=SimpleNamespace(id=777),
    )
    await safe_edit(callback, "same", None)
    assert sent == []

    async def broken(self: Message, text: str, **kwargs: Any) -> Message:
        raise TelegramBadRequest(None, "Bad Request: message to edit not found")

    monkeypatch.setattr(Message, "edit_text", broken)
    await safe_edit(callback, "new", None)
    assert sent == [
        {
            "chat_id": 777,
            "text": "new",
            "parse_mode": "HTML",
            "disable_web_page_preview": True,
            "reply_markup": None,
        }
    ]


@pytest.mark.asyncio
async def test_safe_edit_without_message_sends_to_user() -> None:
    sent: list[dict[str, Any]] = []

    class FakeBot:
        async def send_message(self, chat_id: int, text: str, **kwargs: Any) -> None:
            sent.append({"chat_id": chat_id, "text": text, **kwargs})

    callback = SimpleNamespace(
        message=None,
        bot=FakeBot(),
        from_user=SimpleNamespace(id=42),
    )
    await safe_edit(callback, "hello", None)
    assert sent and sent[0]["chat_id"] == 42
    assert sent[0]["text"] == "hello"


@pytest.mark.asyncio
async def test_send_job_card_uses_html_and_actions(repository) -> None:
    job = _job()
    await repository.create_job(job)
    calls: list[dict[str, Any]] = []

    class FakeMessage:
        async def answer(self, text: str, **kwargs: Any) -> None:
            calls.append({"text": text, **kwargs})

    await send_job_card(FakeMessage(), SimpleNamespace(repo=repository), job)
    assert calls and calls[0]["parse_mode"] == "HTML"
    assert calls[0]["reply_markup"] is not None
    assert job.short_id in calls[0]["text"]


@pytest.mark.asyncio
async def test_settings_text_and_help_text(repository, settings) -> None:
    ctx = SimpleNamespace(
        repo=repository,
        settings=settings,
        processor=SimpleNamespace(available=False),
    )
    text = await settings_text(ctx)
    assert "تنظیمات ربات" in text
    assert "تنظیم نشده" in text
    assert "ارسال مستقیم" in text
    assert "غیرفعال" in text

    await repository.set_setting("destination_chat_id", "@my_channel")
    text = await settings_text(ctx)
    assert "@my_channel" in text

    help_output = help_text()
    for command in ("/add", "/status", "/jobs", "/pause", "/resume", "/cancel", "/settings", "/history"):
        assert command in help_output
