"""Golden-parity: `scraper_kit.providers.domclick.serp` ≡ `app.services.scrapers.domclick`.

Strangler-инвариант (#2133): новая scraper_kit-копия domclick-провайдера должна
давать БАЙТ-ИДЕНТИЧНЫЙ результат парсинга старому боевому коду на одинаковом входе.
Развязка (get_scraper_delay→delay_provider, base/exceptions/repair-normalizer→
scraper_kit.*) НЕ меняет распарсенные данные.

Гоняем ОБА модуля на одной BFF-JSON-фикстуре и сравниваем:
  - `_extract_json` (JSON из HTML-обёртки + QRATOR-block detection);
  - `_map_item` (offer → ScrapedLot): студия rooms=0, sentinel 0→None, agency;
  - `_is_geo_ok` (bbox + offerRegionName guard);
  - url-билдеры и чистые хелперы (_build_offers_url/_build_count_url/
    _parse_publish_date/_extract_agency_name).

Идентичность результата = kit-парсер верно скопирован (развязка не задела логику).
"""

from __future__ import annotations

import json
import os
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import patch

# Старый app.services.scrapers.domclick тянет app.core.config.settings=Settings(),
# которому нужен DATABASE_URL. Офлайн-парсинг БД не трогает — фиктивный DSN достаточен.
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")

# НОВЫЙ (scraper_kit) — ruff сортирует scraper_kit (first-party) выше app-импортов.
from scraper_kit.providers.domclick.serp import DomClickScraper as NewDomClickScraper
from scraper_kit.providers.domclick.serp import _build_count_url as new_build_count_url
from scraper_kit.providers.domclick.serp import _build_offers_url as new_build_offers_url
from scraper_kit.providers.domclick.serp import _extract_agency_name as new_extract_agency
from scraper_kit.providers.domclick.serp import _extract_json as new_extract_json
from scraper_kit.providers.domclick.serp import _parse_publish_date as new_parse_publish_date

from app.services.scrapers.domclick import DomClickScraper as OldDomClickScraper
from app.services.scrapers.domclick import _build_count_url as old_build_count_url
from app.services.scrapers.domclick import _build_offers_url as old_build_offers_url
from app.services.scrapers.domclick import _extract_agency_name as old_extract_agency
from app.services.scrapers.domclick import _extract_json as old_extract_json
from app.services.scrapers.domclick import _parse_publish_date as old_parse_publish_date

_FIXTURES = Path(__file__).parent / "fixtures"
_BFF_JSON = (_FIXTURES / "domclick_bff_offers_sample.json").read_text(encoding="utf-8")
_BFF_ITEMS: list[dict[str, Any]] = json.loads(_BFF_JSON)["result"]["items"]


def _make_old() -> OldDomClickScraper:
    """Старый DomClickScraper без DB: patch get_scraper_delay."""
    with patch("app.services.scrapers.domclick.get_scraper_delay", return_value=8.0):
        return OldDomClickScraper()


def _make_new() -> NewDomClickScraper:
    """Новый DomClickScraper с инжектируемым ScraperConfig + delay_provider.

    Парсинг-тесты не дёргают fetch_city → browser_http_endpoint не используется,
    но конструктор требует config (duck-typed namespace достаточен).
    """
    config = SimpleNamespace(browser_http_endpoint="http://tradein-browser:9000")
    return NewDomClickScraper(config, delay_provider=lambda _name: 8.0)


# ── _extract_json parity ──────────────────────────────────────────────────────


def test_extract_json_parity_bare() -> None:
    """_extract_json на bare JSON-теле идентичен."""
    assert old_extract_json(_BFF_JSON) == new_extract_json(_BFF_JSON)


def test_extract_json_parity_pre_wrapped() -> None:
    """_extract_json на <pre>-обёрнутом JSON идентичен (реальная форма ответа)."""
    wrapped = f"<html><body><pre>{_BFF_JSON}</pre></body></html>"
    assert old_extract_json(wrapped) == new_extract_json(wrapped)


def test_extract_json_parity_qrator_block() -> None:
    """QRATOR block-страница → DomClickBlockedError в обоих (идентичное поведение)."""
    from scraper_kit.domclick_exceptions import DomClickBlockedError as NewBlocked

    from app.services.scrapers.domclick_exceptions import DomClickBlockedError as OldBlocked

    block_html = "<html><body>Система защиты QRATOR: captcha</body></html>"
    old_raised = new_raised = False
    try:
        old_extract_json(block_html)
    except OldBlocked:
        old_raised = True
    try:
        new_extract_json(block_html)
    except NewBlocked:
        new_raised = True
    assert old_raised and new_raised


# ── _map_item parity (offer → ScrapedLot) ─────────────────────────────────────


def test_map_item_parity_all_items() -> None:
    """Каждый offer-item → ScrapedLot идентичен (студия rooms=0, sentinel 0→None)."""
    old = _make_old()
    new = _make_new()
    for item in _BFF_ITEMS:
        # force_rooms=0 для студий (st-бакет) — проверяем обе ветки маппинга.
        for force_rooms in (None, 0):
            old_lot = old._map_item(item, force_rooms=force_rooms)
            new_lot = new._map_item(item, force_rooms=force_rooms)
            if old_lot is None or new_lot is None:
                assert old_lot is None and new_lot is None
                continue
            assert old_lot.model_dump() == new_lot.model_dump()
    # Счётчик parse-ошибок эволюционирует одинаково.
    assert old.parse_failures == new.parse_failures


# ── _is_geo_ok parity (bbox + region guard) ───────────────────────────────────


def test_is_geo_ok_parity_all_items() -> None:
    """Гео-гард (offerRegionName + bbox) идентичен на всех items."""
    old = _make_old()
    new = _make_new()
    for item in _BFF_ITEMS:
        assert old._is_geo_ok(item) == new._is_geo_ok(item)


# ── url-билдеры + чистые хелперы ──────────────────────────────────────────────


def test_url_builders_parity() -> None:
    """_build_offers_url / _build_count_url идентичны (вкл. '5+' → '5%2B')."""
    cases: list[tuple[str, int | None, int | None]] = [
        ("st", None, None),
        ("2", 3_000_000, 8_000_000),
        ("5+", 15_000_000, None),
        ("1", None, 5_000_000),
    ]
    for rooms, gte, lte in cases:
        assert old_build_count_url(rooms, gte, lte) == new_build_count_url(rooms, gte, lte)
        for offset in (0, 20, 1980):
            assert old_build_offers_url(rooms, gte, lte, offset) == new_build_offers_url(
                rooms, gte, lte, offset
            )


def test_parse_publish_date_parity() -> None:
    """_parse_publish_date идентичен на валидных/битых ISO-строках."""
    for iso in [None, "", "2026-06-28T09:15:00+05:00", "2026-06-27", "not-a-date", "2026-13-40"]:
        assert old_parse_publish_date(iso) == new_parse_publish_date(iso)


def test_extract_agency_name_parity() -> None:
    """_extract_agency_name идентичен на строковых/dict/пустых seller-блоках."""
    sellers: list[dict[str, Any]] = [
        {},
        {"company": "АН Этажи", "agent": "Иванов"},
        {"company": "   ", "agent": {"name": "Резерв"}},
        {"agent": {"fullName": "Петров П."}},
        {"company": {"title": "СберРиелт"}},
        {"agent": 12345},
    ]
    for seller in sellers:
        assert old_extract_agency(seller) == new_extract_agency(seller)


# ── strangler-guard: kit-провайдер не импортирует app.* ───────────────────────


def test_kit_domclick_has_no_app_imports() -> None:
    """Ни один модуль scraper_kit.providers.domclick.* не импортирует app.* (развязка)."""
    import inspect

    from scraper_kit.providers.domclick import serp

    source = inspect.getsource(serp)
    for line in source.splitlines():
        stripped = line.strip()
        assert not stripped.startswith("from app"), f"serp: {line!r}"
        assert not stripped.startswith("import app"), f"serp: {line!r}"
