"""404 / removed-listing detection на detail-странице (#2034).

Lat-null pending-очередь backfill в основном содержит МЁРТВЫЕ листинги (404 — потому
они и стали координатными дырами). В browser-mode 404-страница Avito («Ошибка 404.
Страница не найдена», тело тоже содержит generic «Объявления на сайте Авито») не имеет
item-view → раньше ловилась как soft-block → AvitoBlockedError → backfill крутил
consecutive-block breaker и абортил run ДО live-листингов (run 458: attempted=5
enriched=0 blocked=5 → abort).

_is_detail_not_found детектит её ОТДЕЛЬНО → fetch_detail кидает AvitoListingGoneError
(НЕ AvitoBlockedError), backfill метит листинг is_active=FALSE и продолжает без
ротации/ретрая.

Зеркало fixture-стиля test_avito_detail_soft_block.py / test_avito_detail_browser_only.py.
"""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from app.services.scrapers import avito_detail as detail_mod
from app.services.scrapers.avito_detail import fetch_detail
from app.services.scrapers.avito_exceptions import AvitoBlockedError, AvitoListingGoneError

_SENTINEL = object()

# 404-страница Avito: специфичный <title>; тело тоже содержит generic-витринный текст,
# который сам по себе тригерит soft-block — поэтому not-found проверяется ПЕРВЫМ.
_NOT_FOUND_HTML = (
    '<!DOCTYPE html><html lang="ru"><head>'
    "<title>Ошибка 404. Страница не найдена</title>"
    "</head><body><div>Объявления на сайте Авито</div></body></html>"
)
# Soft-block deflect: generic-витрина БЕЗ 404-текста — это блок, НЕ gone.
_SOFT_BLOCK_HTML = (
    '<!DOCTYPE html><html lang="ru"><head>'
    "<title>Авито — Объявления на сайте Авито</title>"
    "</head><body><div>...</div></body></html>"
)
_ITEM_VIEW_HTML = "<html><body><span data-marker='item-view/item-id'>№ 123</span></body></html>"

_ASYNC_SESSION = "app.services.scrapers.avito_detail.AsyncSession"
_BUILD_SESSION = "app.services.scrapers.avito_detail._build_detail_session"
_PARSE = "app.services.scrapers.avito_detail.parse_detail_html"


def test_is_detail_not_found_unit() -> None:
    """Юнит _is_detail_not_found: 404 → True; soft-block deflect / item-view / пусто → False."""
    assert detail_mod._is_detail_not_found(_NOT_FOUND_HTML) is True
    # soft-block deflect (generic-витрина без 404-текста) НЕ должен ловиться как gone
    assert detail_mod._is_detail_not_found(_SOFT_BLOCK_HTML) is False
    # реальное объявление с item-view → False
    assert detail_mod._is_detail_not_found(_ITEM_VIEW_HTML) is False
    assert detail_mod._is_detail_not_found("") is False


def _browser_returning(html: str) -> MagicMock:
    bf = MagicMock()
    bf.fetch = AsyncMock(return_value=html)
    return bf


@pytest.mark.asyncio
async def test_browser_mode_404_raises_listing_gone_not_blocked() -> None:
    """browser_fetcher вернул 404-HTML → AvitoListingGoneError (НЕ AvitoBlockedError),
    без curl-сессии."""
    bf = _browser_returning(_NOT_FOUND_HTML)
    with (
        patch(_ASYNC_SESSION) as async_session,
        patch(_BUILD_SESSION) as build_session,
        patch(_PARSE, return_value=_SENTINEL) as parse,
    ):
        with pytest.raises(AvitoListingGoneError) as exc_info:
            await fetch_detail("/items/123", browser_fetcher=bf)

    # Тип строго AvitoListingGoneError и НЕ подкласс AvitoBlockedError —
    # иначе backfill-ловушка `except (AvitoBlockedError, ...)` перехватила бы его.
    assert not isinstance(exc_info.value, AvitoBlockedError)
    bf.fetch.assert_awaited_once()
    parse.assert_not_called()
    async_session.assert_not_called()
    build_session.assert_not_called()
