"""Audit-scrapers finding 2: Cian totalOffers vs results.offers length mismatch.

`_parse_serp_html` извлекает `totalOffers` и `results.offers` из ОДНОГО Redux
state-блоба (одна SSR-выдача). Раньше `results.offers` пустой при `totalOffers>0`
логировался WARNING'ом и тихо возвращался `[]` — не считался schema-regression, не
попадал в мониторинг (`_report_schema_regression`/Glitchtip).

Порог: 0 vs >0 — единственный позиционно-независимый сигнал, который можно
проверить без номера страницы внутри `_parse_serp_html` (эта функция не знает,
какая это страница пагинации — дробный порог типа "< 50% от totalOffers" ложно
сработал бы на легитимной последней частичной странице exhaustive-пагинации,
которую эта функция не различает). totalOffers=0 (реально пустой поиск) НЕ
считается регрессией.

Refs: audit-scrapers 2026-07-26, finding 2 (low).
"""

from __future__ import annotations

import os
from unittest.mock import MagicMock, patch

os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

from scraper_kit.providers.cian.serp import CianScraper

from app.services.scraper_adapters import RealScraperConfig


def _scraper() -> CianScraper:
    return CianScraper(RealScraperConfig())


def test_total_offers_positive_but_offers_empty_reports_regression() -> None:
    """totalOffers=5, results.offers=[] — internal contradiction, must report."""
    s = _scraper()
    state = {"results": {"totalOffers": 5, "offers": []}}
    with patch("scraper_kit.providers.cian.serp.extract_state", return_value=state):
        with patch.object(s, "_report_schema_regression") as mock_report:
            lots = s._parse_serp_html("<html>irrelevant</html>")

    assert lots == []
    mock_report.assert_called_once()
    (msg,), _ = mock_report.call_args
    assert "totalOffers=5" in msg


def test_total_offers_zero_and_offers_empty_is_valid_empty_search() -> None:
    """totalOffers=0, offers=[] — легитимная пустая выборка, НЕ регрессия."""
    s = _scraper()
    state = {"results": {"totalOffers": 0, "offers": []}}
    with patch("scraper_kit.providers.cian.serp.extract_state", return_value=state):
        with patch.object(s, "_report_schema_regression") as mock_report:
            lots = s._parse_serp_html("<html>irrelevant</html>")

    assert lots == []
    mock_report.assert_not_called()


def test_total_offers_none_and_offers_empty_is_not_reported_as_regression() -> None:
    """totalOffers отсутствует/None в state — недостаточно сигнала для regression-репорта
    (могла быть частично битая state-структура без явного totalOffers>0 контр-сигнала)."""
    s = _scraper()
    state = {"results": {"offers": []}}
    with patch("scraper_kit.providers.cian.serp.extract_state", return_value=state):
        with patch.object(s, "_report_schema_regression") as mock_report:
            lots = s._parse_serp_html("<html>irrelevant</html>")

    assert lots == []
    mock_report.assert_not_called()


def test_offers_present_normal_path_unaffected() -> None:
    """totalOffers=1, offers содержит 1 запись без cianId/id — не проходит
    _offer_to_lot, но это уже существующая (0/N offer-level) охрана, не finding 2."""
    s = _scraper()
    state = {"results": {"totalOffers": 1, "offers": [{"noId": True}]}}
    with patch("scraper_kit.providers.cian.serp.extract_state", return_value=state):
        with patch.object(s, "_report_schema_regression") as mock_report:
            lots = s._parse_serp_html("<html>irrelevant</html>")

    # offers_data непустой → finding 2 guard не участвует; существующая offer-level
    # охрана (raw_count>0 and saved_count==0) должна отработать вместо неё.
    assert lots == []
    mock_report.assert_called_once()
    (msg,), _ = mock_report.call_args
    assert "_offer_to_lot" in msg


def test_state_none_extraction_failed_no_regression_report() -> None:
    """extract_state вернул None (captcha/структура целиком не найдена) — уже
    существующая ветка, НЕ должна триггерить finding-2 regression report."""
    s = _scraper()
    with patch("scraper_kit.providers.cian.serp.extract_state", return_value=None):
        with patch.object(s, "_report_schema_regression") as mock_report:
            lots = s._parse_serp_html("<html>irrelevant</html>")

    assert lots == []
    mock_report.assert_not_called()


def test_report_schema_regression_swallows_missing_glitchtip_dsn() -> None:
    """_report_schema_regression не должен падать, если glitchtip_dsn не настроен."""
    s = _scraper()
    s._config = MagicMock(glitchtip_dsn=None)
    s._report_schema_regression("test message")  # не должно бросить исключение
