"""Tests for resolve_cian_zhk_url_via_search (#972) — the WORKING nb_id → ЖК-url resolver.

The legacy resolve_cian_zhk_url hit https://cian.ru/zhk/<id>/ which now 404s; the
search-based resolver fetches the cat.php newbuilding-SERP and extracts the canonical
zhk-<slug>.cian.ru URL from its markup. No real network: the curl_cffi session is mocked;
the regex is exercised against a trimmed real-prod SERP fixture.

Легаси `app.services.scrapers.cian_newbuilding` удалён (#2397 финальный шаг E, 0
runtime-импортёров) — переведено на kit-эквивалент `scraper_kit.providers.cian.
newbuilding` (byte-identical strangler-копия).
"""

from __future__ import annotations

import os
import sys
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock

# DATABASE_URL required by config before any app import.
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
# WeasyPrint stub — not installed in CI without GTK; pulled in transitively by app imports.
sys.modules.setdefault("weasyprint", MagicMock())

import pytest  # noqa: E402
from scraper_kit.providers.cian.newbuilding import (  # noqa: E402
    _extract_zhk_url_from_serp,
    resolve_cian_zhk_url_via_search,
)

_FIXTURE = Path(__file__).resolve().parents[2] / "fixtures" / "cian_catph_newbuilding_serp.html"


def _serp_fixture() -> str:
    return _FIXTURE.read_text(encoding="utf-8")


def _mock_session(*, status_code: int, text: str) -> MagicMock:
    """A curl_cffi AsyncSession stand-in returning one canned response."""
    session = MagicMock()
    response = MagicMock()
    response.status_code = status_code
    response.text = text
    session.get = AsyncMock(return_value=response)
    session.close = AsyncMock()
    return session


# ---- pure regex extractor -------------------------------------------------


def test_extract_zhk_url_from_real_serp_fixture() -> None:
    """The trimmed prod cat.php SERP yields the canonical ЖК-slug URL (nb 48853).

    The fixture carries the real <h1 data-name="Title"> header block, so this exercises
    the title-anchored extraction path against real prod markup.
    """
    url = _extract_zhk_url_from_serp(_serp_fixture())
    assert url == "https://zhk-parkovyy-kvartal-ekb-i.cian.ru"


def test_extract_zhk_url_anchors_on_title_not_recommendation() -> None:
    """Adversarial (#972 hardening): a recommendation / "похожие ЖК" block embedding a
    FOREIGN zhk-slug appears BEFORE the title block whose anchor is the queried ЖК.

    A naive first-match would resolve the recommendation's `zhk-other-ad` and then enrich
    the house with the WRONG building's price_dynamics/reliability (which the backfill's
    idempotency would lock in). The title-anchored extractor must return the QUERIED ЖК
    (`zhk-target`), proving the foreign url before the title is ignored.
    """
    html = (
        '<div data-name="Breadcrumbs">'
        '<a href="https://www.cian.ru/kupit/">Продажа</a></div>'
        # Banner / recommendation block rendered BEFORE the title, carrying a foreign slug.
        '<div data-name="Recommendations">'
        '<a href="https://zhk-other-ad-ekb-i.cian.ru/">похожий ЖК (реклама)</a>'
        "and a bare one too https://zhk-another-promo-ekb-i.cian.ru in text</div>"
        # The canonical title block — its anchor is the queried ЖК.
        '<div data-name="NewbuildingHeaderSeo">'
        '<h1 data-name="Title" class="x31de4314--_7735e">Купить квартиру в '
        '<a href="https://zhk-target-ekb-i.cian.ru/" target="_blank" '
        'rel="noreferrer" class="x31de4314--_9d71c--link">ЖК «Целевой»</a></h1></div>'
        # Trailing offer cards repeating the target slug (как на реальном SERP).
        '<a data-name="LinkArea" href="https://zhk-target-ekb-i.cian.ru/kvartiry/">x</a>'
    )
    assert _extract_zhk_url_from_serp(html) == "https://zhk-target-ekb-i.cian.ru"


def test_extract_zhk_url_fallback_first_match_when_no_title() -> None:
    """Markup drift: no <h1 data-name="Title"> header → degrade to naive first-match.

    The title anchor is the primary path; when it is absent (header renamed/moved) the
    extractor falls back to the first zhk-slug occurrence so resolution still yields
    *something* rather than failing outright.
    """
    html = (
        'noise <a href="https://zhk-first-ekb-i.cian.ru/">A</a> '
        "and later https://zhk-second-ekb-i.cian.ru more"
    )
    assert _extract_zhk_url_from_serp(html) == "https://zhk-first-ekb-i.cian.ru"


def test_extract_zhk_url_no_match_returns_none() -> None:
    """A SERP with no zhk-slug anchor (empty results / drift) → None."""
    assert _extract_zhk_url_from_serp("<html><body>Ничего не найдено</body></html>") is None
    # A bare cian.ru link that is NOT a zhk-* subdomain must not match.
    assert _extract_zhk_url_from_serp('<a href="https://www.cian.ru/kupit/">x</a>') is None


# ---- async resolver (session mocked) --------------------------------------


@pytest.mark.asyncio
async def test_resolve_via_search_happy_path() -> None:
    """HTTP 200 + a SERP containing the slug → canonical url; session reused, not closed."""
    session = _mock_session(status_code=200, text=_serp_fixture())
    url = await resolve_cian_zhk_url_via_search(48853, session=session)
    assert url == "https://zhk-parkovyy-kvartal-ekb-i.cian.ru"
    # caller owns a passed-in session — resolver must NOT close it.
    session.close.assert_not_awaited()
    # the requested URL targets the cat.php newbuilding SERP for this nb_id.
    called_url = session.get.call_args.args[0]
    assert "cat.php" in called_url
    assert "newobject[0]=48853" in called_url


@pytest.mark.asyncio
async def test_resolve_via_search_no_match_returns_none() -> None:
    """HTTP 200 but no zhk-slug in the body → None (empty SERP / markup drift)."""
    session = _mock_session(status_code=200, text="<html>no results</html>")
    assert await resolve_cian_zhk_url_via_search(999, session=session) is None


@pytest.mark.asyncio
async def test_resolve_via_search_non_200_returns_none() -> None:
    """A non-200 (block / 404) → None without attempting extraction."""
    session = _mock_session(status_code=429, text="too many requests")
    assert await resolve_cian_zhk_url_via_search(48853, session=session) is None


@pytest.mark.asyncio
async def test_resolve_via_search_swallows_request_error() -> None:
    """A transport error inside the fetch → None (logged), never propagates."""
    session = MagicMock()
    session.get = AsyncMock(side_effect=ConnectionError("proxy refused"))
    session.close = AsyncMock()
    assert await resolve_cian_zhk_url_via_search(48853, session=session) is None
