"""Parity tests for issue #2306 (Group B of scraper_kit migration epic #2277).

Migrates the INTERNAL legacy-scraper imports of three product-side files to their
`scraper_kit` equivalents (public API of the three files is unchanged — no caller
elsewhere in the codebase needed updating, see vault audit
`Scraper_Kit_Legacy_Dependency_Audit_0703`):

  - app/services/cian_price_history.py  → fetch_detail/save_detail_enrichment now from
    `scraper_kit.providers.cian.detail` (was `app.services.scrapers.cian_detail`).
  - app/services/cian_session.py        → extract_state now from
    `scraper_kit.cian_state_parser` (was `app.services.scrapers.cian_state_parser`).
  - app/services/yandex_price_history.py → ScrapedLot now from `scraper_kit.base`
    (was `app.services.scrapers.base`).

This file proves the DELTA introduced by the migration specifically — general
cian-provider / ScrapedLot golden-parity is already covered by
`tests/test_scraper_kit_cian_golden_parity.py` and
`tests/test_scraper_kit_base_golden_parity.py`. Uses `tests/support/parity.assert_parity`
per the epic's parity-gate convention (issue #2304).
"""

from __future__ import annotations

import asyncio
import dataclasses
import json
import os
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

# Старый app.services.scrapers.cian* / app.core.config.settings требует DATABASE_URL
# при импорте (тот же паттерн, что в test_scraper_kit_cian_golden_parity.py).
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")

# Kit — то, на что теперь ссылаются мигрированные файлы (#2306).
from scraper_kit.base import ScrapedLot as KitScrapedLot
from scraper_kit.cian_state_parser import extract_state as kit_extract_state
from scraper_kit.providers.cian.detail import fetch_detail as kit_fetch_detail

from app.core.config import settings
from app.services.scraper_adapters import RealScraperConfig

# Legacy — эталон сравнения (эти модули НЕ меняются в #2306).
from app.services.scrapers.base import ScrapedLot as LegacyScrapedLot
from app.services.scrapers.cian_detail import fetch_detail as legacy_fetch_detail
from app.services.scrapers.cian_state_parser import extract_state as legacy_extract_state
from tests.support.parity import assert_parity

# ── extract_state parity (cian_session.py dependency) ─────────────────────────


def _auth_state_html() -> str:
    state = {"user": {"isAuthenticated": True, "userId": 42}}
    state_json = json.dumps(state, ensure_ascii=False)
    return (
        "<html><body><script>\n"
        "window._cianConfig['header-frontend'] = window._cianConfig['header-frontend'] || [];\n"
        "window._cianConfig['header-frontend'].push({"
        f"key: 'initialState', value: {state_json}, priority: 10, filter: null"
        "});\n</script></body></html>"
    )


def test_extract_state_parity_on_match() -> None:
    """cian_session.py._classify_verify_response reads this exact (mfe, key) pair."""
    html = _auth_state_html()
    assert_parity(
        legacy_fn=legacy_extract_state,
        kit_fn=kit_extract_state,
        fixtures=[(html, "header-frontend", "initialState")],
    )


def test_extract_state_parity_on_miss() -> None:
    """Missing mfe/key → both return None identically (not just on the happy path)."""
    html = _auth_state_html()
    assert_parity(
        legacy_fn=legacy_extract_state,
        kit_fn=kit_extract_state,
        fixtures=[(html, "frontend-offer-card", "defaultState")],
    )


# ── ScrapedLot parity (yandex_price_history.py dependency) ────────────────────


def _lot_kwargs() -> dict[str, Any]:
    return {
        "source": "yandex",
        "source_url": "https://realty.yandex.ru/offer/123/",
        "source_id": "123",
        "price_rub": 5_000_000,
        "price_previous_rub": 5_200_000,
        "price_trend": "DECREASED",
    }


def test_scraped_lot_model_dump_parity() -> None:
    """record_yandex_price_history duck-types lot.source/.source_id/.price_rub/...

    Legacy- и kit-ScrapedLot должны давать идентичный model_dump() на одном входе —
    доказывает, что смена типа импорта в yandex_price_history.py не меняет runtime-
    поведение (функция принимает оба класса взаимозаменяемо).
    """
    kwargs = _lot_kwargs()
    assert_parity(
        legacy_fn=lambda kw: LegacyScrapedLot(**kw).model_dump(),
        kit_fn=lambda kw: KitScrapedLot(**kw).model_dump(),
        fixtures=[kwargs],
    )


# ── fetch_detail / DetailEnrichment parity (cian_price_history.py dependency) ──


def _detail_html() -> str:
    default_state = {
        "offerData": {
            "offer": {
                "cianId": 555111,
                "priceChanges": [
                    {"changeTime": "2026-05-01T00:00:00Z", "price": 6100000, "diffPercent": -1.2}
                ],
            },
            "stats": {},
        }
    }
    ds_json = json.dumps(default_state, ensure_ascii=False)
    return (
        "<html><body><script>\n"
        "window._cianConfig['frontend-offer-card'] = "
        "window._cianConfig['frontend-offer-card'] || [];\n"
        "window._cianConfig['frontend-offer-card'].push({"
        f"key: 'defaultState', value: {ds_json}, priority: 10, filter: null"
        "});\n</script></body></html>"
    )


def _mock_session(html: str) -> MagicMock:
    response = MagicMock()
    response.status_code = 200
    response.text = html
    session = MagicMock()
    session.get = AsyncMock(return_value=response)
    session.close = AsyncMock()
    return session


def _run_legacy_fetch(url: str, html: str) -> Any:
    return dataclasses.asdict(asyncio.run(legacy_fetch_detail(url, session=_mock_session(html))))


def _run_kit_fetch(url: str, html: str) -> Any:
    # Точно тот вызов, что теперь делает cian_price_history.backfill_cian_price_history:
    # config=RealScraperConfig() добавлен по миграции (#2306); session= общий mock здесь
    # только чтобы избежать сетевого I/O — сам config не участвует в session-пути, так
    # что этот тест доказывает: инжекция config= не меняет результат парсинга.
    result = asyncio.run(
        kit_fetch_detail(url, config=RealScraperConfig(), session=_mock_session(html))
    )
    return dataclasses.asdict(result)


def test_fetch_detail_with_config_injection_parity() -> None:
    html = _detail_html()
    url = "https://ekb.cian.ru/sale/flat/555111/"
    assert_parity(
        legacy_fn=_run_legacy_fetch,
        kit_fn=_run_kit_fetch,
        fixtures=[(url, html)],
    )


def test_fetch_detail_own_session_proxy_wiring_parity(monkeypatch: Any) -> None:
    """cian_price_history calls fetch_detail WITHOUT session= (own-session curl_cffi path).

    Kit fetch_detail without config= silently drops the proxy (direct connection) — a
    real behaviour regression risk (#806: datacenter-IP gets banned by Cian without a
    proxy). This proves config=RealScraperConfig() (our actual call site) constructs
    AsyncSession(proxies=...) identically to the legacy direct-settings path.
    """
    monkeypatch.setattr(settings, "cian_proxy_url_env", "http://test-proxy.local:8080")

    captured: dict[str, dict[str, Any]] = {}

    def _make_fake_session(name: str) -> Any:
        class _FakeSession:
            def __init__(self, **kwargs: Any) -> None:
                captured[name] = kwargs

            async def get(self, *_args: Any, **_kwargs: Any) -> Any:
                response = MagicMock()
                response.status_code = 200
                response.text = _detail_html()
                return response

            async def close(self) -> None:
                pass

        return _FakeSession

    url = "https://ekb.cian.ru/sale/flat/1/"

    with patch("app.services.scrapers.cian_detail.AsyncSession", _make_fake_session("legacy")):
        asyncio.run(legacy_fetch_detail(url))

    with patch("scraper_kit.providers.cian.detail.AsyncSession", _make_fake_session("kit")):
        asyncio.run(kit_fetch_detail(url, config=RealScraperConfig()))

    assert captured["legacy"].get("proxies") == captured["kit"].get("proxies")
    assert captured["kit"]["proxies"] == {
        "http": "http://test-proxy.local:8080",
        "https": "http://test-proxy.local:8080",
    }
