"""Тесты для extraction plan_image_url из страницы квартиры DOM.РФ (issue #2440).

Покрывает:
  - __NEXT_DATA__ pageProps (primary): URL плана под plan-hint ключом.
  - bare <img> fallback: plan-hint в атрибутах / proximity к «Планировка».
  - нормализация URL (относительный → absolute; data:-inline / шум отбрасываются).
  - интеграция с parse_catalog_flat: ключ plan_image_url в результате.
  - _TextCollector НЕ ломает text-extraction при наличии <img> (регрессия #1608).

Фикстуры инлайновые (минимальный реалистичный HTML), без live-fetch — DOM.РФ
каталог-квартир это Next.js SSR (тот же стек что страница объекта в
domrf_catalog_object.py, где план живёт в __NEXT_DATA__, а не в <img src>).
"""

from __future__ import annotations

import json

import pytest

from app.services.scrapers.domrf_catalog import (
    _normalize_plan_url,
    _TextCollector,
    extract_plan_image_url,
    parse_catalog_flat,
)
from app.services.scrapers.stealth import BASE_URL

# Абсолютный file-API URL DOM.РФ (реальный паттерн, см. documents.py:17).
PLAN_ABS_URL = f"{BASE_URL}/api/ext/file/plan-12345.png"
# Относительный next/image URL (Next.js image optimizer).
PLAN_REL_NEXT = "/_next/image?url=%2Fapi%2Fext%2Ffile%2Fplan-777.png&w=1200&q=75"


def _html_with_next_data(page_props: dict) -> str:
    """SSR HTML с __NEXT_DATA__ блоком, содержащим pageProps."""
    blob = json.dumps({"props": {"pageProps": page_props}})
    return (
        "<!doctype html><html><head>"
        f'<script id="__NEXT_DATA__" type="application/json">{blob}</script>'
        "</head><body>"
        "<h1>Квартира 2-комнатная</h1>"
        "<div>4 500 000 ₽</div>"
        "</body></html>"
    )


def _collector_for(html: str) -> _TextCollector:
    c = _TextCollector()
    c.feed(html)
    return c


# ── __NEXT_DATA__ primary path ────────────────────────────────────────────────


def test_plan_from_next_data_plan_image_key() -> None:
    """URL под ключом planImage в pageProps → извлекается (absolute)."""
    html = _html_with_next_data({"planImage": PLAN_ABS_URL, "buildingClass": "Комфорт"})
    assert extract_plan_image_url(html, _collector_for(html)) == PLAN_ABS_URL


def test_plan_from_next_data_layout_key_nested() -> None:
    """URL под вложенным ключом layoutImageUrl → извлекается (BFS обход)."""
    html = _html_with_next_data(
        {"flat": {"info": {"layoutImageUrl": PLAN_ABS_URL}}, "price": 4_500_000}
    )
    assert extract_plan_image_url(html, _collector_for(html)) == PLAN_ABS_URL


def test_plan_from_next_data_relative_url_normalized() -> None:
    """Относительный next/image URL → абсолютный через BASE_URL."""
    html = _html_with_next_data({"floorPlanUrl": PLAN_REL_NEXT})
    got = extract_plan_image_url(html, _collector_for(html))
    assert got is not None
    assert got.startswith(BASE_URL)
    assert "plan-777" in got


def test_plan_from_next_data_ignores_non_plan_keys() -> None:
    """Картинка под НЕ-plan ключом (developerLogo) не должна ловиться."""
    html = _html_with_next_data(
        {"developerLogo": f"{BASE_URL}/api/ext/file/logo.png", "buildingClass": "Бизнес"}
    )
    assert extract_plan_image_url(html, _collector_for(html)) is None


def test_plan_from_next_data_ignores_noise_url() -> None:
    """plan-ключ, но URL содержит logo/thumb (шум) → отбрасывается."""
    html = _html_with_next_data({"planThumb": f"{BASE_URL}/api/ext/file/plan-thumb.png"})
    # 'thumb' в URL → _IMAGE_NOISE_RE → None
    assert extract_plan_image_url(html, _collector_for(html)) is None


# ── bare <img> fallback ───────────────────────────────────────────────────────


def test_plan_from_img_class_hint() -> None:
    """Нет __NEXT_DATA__: <img class='flat-plan__image'> → извлекается по class."""
    html = (
        "<!doctype html><html><body>"
        f'<img class="flat-plan__image" alt="" src="{PLAN_ABS_URL}">'
        "</body></html>"
    )
    assert extract_plan_image_url(html, _collector_for(html)) == PLAN_ABS_URL


def test_plan_from_img_src_hint() -> None:
    """<img> без plan-класса, но src содержит 'plan' → извлекается."""
    html = f'<!doctype html><html><body><img src="{PLAN_ABS_URL}"></body></html>'
    assert extract_plan_image_url(html, _collector_for(html)) == PLAN_ABS_URL


def test_plan_from_img_proximity_to_label() -> None:
    """<img> без plan-hint в атрибутах, но рядом с блоком «Планировка»."""
    plain = f"{BASE_URL}/api/ext/file/imgabc.png"
    html = (
        "<!doctype html><html><body>"
        "<div>Планировка</div>"
        f'<img alt="" src="{plain}">'
        "</body></html>"
    )
    got = extract_plan_image_url(html, _collector_for(html))
    assert got == plain


def test_plan_from_img_skips_logo_and_captcha() -> None:
    """Лого/captcha <img> игнорируются, даже если стоят первыми."""
    html = (
        "<!doctype html><html><body>"
        f'<img class="header-logo" src="{BASE_URL}/api/ext/file/logo.png">'
        '<img class="robot" src="data:image/jpeg;base64,AAAA">'
        "<div>Планировка квартиры</div>"
        f'<img class="plan-view" src="{PLAN_ABS_URL}">'
        "</body></html>"
    )
    assert extract_plan_image_url(html, _collector_for(html)) == PLAN_ABS_URL


def test_no_plan_returns_none() -> None:
    """Нет ни __NEXT_DATA__ плана, ни plan-img → None."""
    html = (
        "<!doctype html><html><body>"
        f'<img class="header-logo" src="{BASE_URL}/api/ext/file/logo.png">'
        "<p>Площадь 45 м²</p>"
        "</body></html>"
    )
    assert extract_plan_image_url(html, _collector_for(html)) is None


# ── URL normalization ─────────────────────────────────────────────────────────


@pytest.mark.parametrize(
    "raw,expected_prefix",
    [
        (PLAN_ABS_URL, PLAN_ABS_URL),
        ("/api/ext/file/plan.png", f"{BASE_URL}/api/ext/file/plan.png"),
        ("//cdn.example/plan.png", "https://cdn.example/plan.png"),
    ],
)
def test_normalize_plan_url_variants(raw: str, expected_prefix: str) -> None:
    assert _normalize_plan_url(raw) == expected_prefix


@pytest.mark.parametrize("raw", ["", None, "data:image/png;base64,AAAA", "   "])
def test_normalize_plan_url_rejects_junk(raw: str | None) -> None:
    assert _normalize_plan_url(raw) is None


# ── integration: parse_catalog_flat ───────────────────────────────────────────


def test_parse_catalog_flat_includes_plan_image_url() -> None:
    """parse_catalog_flat кладёт plan_image_url в результат при наличии плана."""
    html = _html_with_next_data({"planImage": PLAN_ABS_URL, "price": 4_500_000})
    result = parse_catalog_flat(html)
    assert result.get("plan_image_url") == PLAN_ABS_URL


def test_parse_catalog_flat_omits_plan_when_absent() -> None:
    """Нет плана → ключ plan_image_url отсутствует (а не None)."""
    html = "<!doctype html><html><body><p>Площадь 45 м²</p></body></html>"
    result = parse_catalog_flat(html)
    assert "plan_image_url" not in result


# ── regression: <img> capture must not break text-extraction (#1608) ──────────


def test_img_capture_preserves_text_blocks() -> None:
    """Наличие <img> (void-тег) не рассинхронизирует стек — текст блоков цел."""
    html = (
        "<!doctype html><html><body>"
        '<div class="price-block">4 500 000 ₽</div>'
        f'<img src="{PLAN_ABS_URL}">'
        '<div class="status-badge">Продана</div>'
        "</body></html>"
    )
    collector = _collector_for(html)
    texts = [t for _cls, t in collector.blocks]
    assert any("4 500 000" in t for t in texts)
    assert any("Продана" in t for t in texts)
    # и картинка захвачена в отдельный список
    assert len(collector.images) == 1
    assert collector.images[0][0].get("src") == PLAN_ABS_URL

    # parse_catalog_flat всё ещё корректно извлекает status/price при наличии img
    result = parse_catalog_flat(html)
    assert result.get("status") == "sold"
    assert result.get("price_rub") == 4_500_000
