"""Soft-block (HTTP 200 generic-витрина) detection на detail-странице (#1967 / #1950).

Под нагрузкой Avito иногда отдаёт HTTP 200 с ОБЩЕЙ витриной
(<title>Авито — Объявления на сайте Авито</title>) вместо запрошенного объявления —
мягкий анти-бот deflect. У такой страницы нет ни firewall-маркеров, ни item-view →
parse_detail_html ронял ValueError → generic failed, IP НЕ ротировался, enriched
каскадно падал к 0. _is_detail_soft_block детектит её как блок → существующая
403/firewall reconnect-машинерия пересоздаёт эфемерную сессию (свежий exit-IP).

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

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from app.core.config import settings
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

_SENTINEL = object()
_SOFT_BLOCK_HTML = (
    '<!DOCTYPE html><html lang="ru"><head>'
    "<title>Авито — Объявления на сайте Авито</title>"
    "</head><body><div>...</div></body></html>"
)


def _resp(status_code: int, text: str = "") -> MagicMock:
    r = MagicMock()
    r.status_code = status_code
    r.text = text
    return r


def _enable_backconnect(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(settings, "scraper_proxy_url_env", "http://u:p@mproxy.site:14619")


@pytest.fixture(autouse=True)
def _patch_common() -> object:
    """asyncio.sleep → no-op; parse_detail_html → sentinel (нет нужды в реальном HTML)."""
    with (
        patch("app.services.scrapers.avito_detail.asyncio.sleep", AsyncMock()),
        patch(
            "app.services.scrapers.avito_detail.parse_detail_html", return_value=_SENTINEL
        ) as parse,
    ):
        yield parse


@pytest.mark.asyncio
async def test_soft_block_200_reconnect_succeeds(monkeypatch: pytest.MonkeyPatch) -> None:
    """soft-block(HTTP 200) → 200: generic-витрина детектится как блок → reconnect."""
    _enable_backconnect(monkeypatch)

    caller = AsyncMock()
    caller.get = AsyncMock(return_value=_resp(200, _SOFT_BLOCK_HTML))
    retry1 = AsyncMock()
    retry1.get = AsyncMock(return_value=_resp(200, "<html/>"))
    build = MagicMock(side_effect=[retry1])

    with patch.object(detail_mod, "_build_detail_session", build):
        result = await fetch_detail("/x", cffi_session=caller)

    assert result is _SENTINEL
    assert build.call_count == 1
    caller.close.assert_not_called()


@pytest.mark.asyncio
async def test_soft_block_exhausted_raises_blocked(monkeypatch: pytest.MonkeyPatch) -> None:
    """soft-block на каждом attempt _AVITO_DETAIL_403_MAX_RETRIES+1 раз → AvitoBlockedError."""
    _enable_backconnect(monkeypatch)

    caller = AsyncMock()
    caller.get = AsyncMock(return_value=_resp(200, _SOFT_BLOCK_HTML))

    def _make_retry() -> AsyncMock:
        s = AsyncMock()
        s.get = AsyncMock(return_value=_resp(200, _SOFT_BLOCK_HTML))
        return s

    build = MagicMock(side_effect=lambda: _make_retry())

    with patch.object(detail_mod, "_build_detail_session", build):
        with pytest.raises(AvitoBlockedError):
            await fetch_detail("/x", cffi_session=caller)

    assert build.call_count == detail_mod._AVITO_DETAIL_403_MAX_RETRIES
    caller.close.assert_not_called()


def test_is_detail_soft_block_unit() -> None:
    """Юнит _is_detail_soft_block: generic-витрина → True; item-view / пусто → False."""
    assert detail_mod._is_detail_soft_block(_SOFT_BLOCK_HTML) is True
    item_view = "<html><body><span data-marker='item-view/item-id'>№ 123</span></body></html>"
    assert detail_mod._is_detail_soft_block(item_view) is False
    assert detail_mod._is_detail_soft_block("<html/>") is False
    assert detail_mod._is_detail_soft_block("") is False
