"""test_avito_imv_browser_transport.py — #915 Stage 3.

Юниты browser-транспорта для IMV-флоу (без сети, без камуфокса):
  1. _BrowserResponse — мимикрия под curl_cffi Response (.status_code/.text/.json/
     .raise_for_status).
  2. _BrowserSessionAdapter — .get/.post вызывают BrowserFetcher.fetch_json с
     корректными (url, method, headers, body, origin); .post сериализует json.
  3. evaluate_via_imv(..., browser_fetcher=mock) — happy path через 4 канонических
     ответа (warm-up GET → coords → position → get-data) → populated IMVEvaluation;
     403 на get-data → IMVAuthError.

browser_fetcher мокается AsyncMock'ом — реальный HTTP/камуфокс не поднимается.

Легаси `app.services.scrapers.avito_imv` удалён (#2277 финальный шаг scraper_kit-
миграции, 0 runtime importers) — тесты переведены на kit-эквивалент
`scraper_kit.providers.avito.imv` без изменения покрытия.
"""

from __future__ import annotations

import asyncio
import json
from typing import Any
from unittest.mock import AsyncMock

import pytest
from scraper_kit.providers.avito.imv import (
    AVITO_BASE,
    WARMUP_URL,
    IMVAuthError,
    _BrowserResponse,
    _BrowserSessionAdapter,
    evaluate_via_imv,
)

# JWT-заглушка (структура неважна — парсер берёт строку как есть в geo.geo_hash).
_FAKE_JWT = "eyJhbGciOiJIUzI1NiJ9.eyJsYXQiOjU2Ljg0fQ.sig"


# ── _BrowserResponse ──────────────────────────────────────────────────────────


def test_browser_response_status_and_text() -> None:
    r = _BrowserResponse(200, '{"ok": true}')
    assert r.status_code == 200
    assert r.text == '{"ok": true}'


def test_browser_response_json_parses_body() -> None:
    r = _BrowserResponse(200, '{"price": 7370000, "nested": {"a": 1}}')
    assert r.json() == {"price": 7370000, "nested": {"a": 1}}


def test_browser_response_raise_for_status_noop_under_400() -> None:
    # < 400 → не бросает (200/302/399).
    _BrowserResponse(200, "ok").raise_for_status()
    _BrowserResponse(399, "ok").raise_for_status()


def test_browser_response_raise_for_status_raises_on_4xx() -> None:
    with pytest.raises(RuntimeError, match="HTTP 404"):
        _BrowserResponse(404, "not found").raise_for_status()


def test_browser_response_403_status_code() -> None:
    # 403 как status_code (категоризацию в IMVAuthError делает _raise_for_status_categorized).
    r = _BrowserResponse(403, "forbidden")
    assert r.status_code == 403


# ── _BrowserSessionAdapter ────────────────────────────────────────────────────


def _make_adapter(return_value: dict) -> tuple[_BrowserSessionAdapter, AsyncMock]:
    bf = AsyncMock()
    bf.fetch_json = AsyncMock(return_value=return_value)
    adapter = _BrowserSessionAdapter(bf, origin=WARMUP_URL)
    return adapter, bf


def test_adapter_get_calls_fetch_json() -> None:
    adapter, bf = _make_adapter({"status": 200, "body": '{"x": 1}'})

    async def _run() -> _BrowserResponse:
        return await adapter.get(
            "https://www.avito.ru/api/x", headers={"Accept": "application/json"}
        )

    resp = asyncio.run(_run())

    bf.fetch_json.assert_awaited_once_with(
        "https://www.avito.ru/api/x",
        method="GET",
        headers={"Accept": "application/json"},
        origin=WARMUP_URL,
    )
    assert resp.status_code == 200
    assert resp.json() == {"x": 1}


def test_adapter_post_serializes_body() -> None:
    adapter, bf = _make_adapter({"status": 201, "body": "{}"})
    payload = {"address": "ЕКБ", "latitude": 56.84, "isRadius": False}

    resp = asyncio.run(
        adapter.post(
            "https://www.avito.ru/js/v2/geo/position",
            headers={"Content-Type": "application/json"},
            json=payload,
        )
    )

    bf.fetch_json.assert_awaited_once()
    _, kwargs = bf.fetch_json.await_args
    assert kwargs["method"] == "POST"
    assert kwargs["headers"] == {"Content-Type": "application/json"}
    assert kwargs["origin"] == WARMUP_URL
    # body должен быть JSON-строкой ровно как json.dumps(payload).
    assert kwargs["body"] == json.dumps(payload)
    assert json.loads(kwargs["body"]) == payload
    assert resp.status_code == 201


def test_adapter_post_none_body_passes_none() -> None:
    adapter, bf = _make_adapter({"status": 200, "body": "{}"})

    asyncio.run(adapter.post("https://www.avito.ru/api/x"))

    _, kwargs = bf.fetch_json.await_args
    assert kwargs["body"] is None


def test_adapter_403_propagates_status_code() -> None:
    adapter, _ = _make_adapter({"status": 403, "body": "forbidden"})

    resp = asyncio.run(adapter.get("https://www.avito.ru/api/x"))
    assert resp.status_code == 403


def test_adapter_close_is_noop() -> None:
    adapter, _ = _make_adapter({"status": 200, "body": "{}"})

    # close() не должен ничего ронять и ничего возвращает.
    assert asyncio.run(adapter.close()) is None


# ── evaluate_via_imv(browser_fetcher=...) — full flow ─────────────────────────


def _canned_responses() -> list[dict]:
    """4 ответа в порядке вызова: warm-up GET → coords (A) → position (B) → get-data (C)."""
    warmup = {"status": 200, "body": "<html>warm</html>"}
    coords_a = {
        "status": 200,
        "body": json.dumps(
            {
                "normalizedAddress": "Свердловская область, Екатеринбург, ул. Чайковского, 66А",
                "point": {"latitude": 56.840872, "longitude": 60.654077},
            }
        ),
    }
    position_b = {
        "status": 200,
        "body": json.dumps(
            {
                "address": "Свердловская область, Екатеринбург, ул. Чайковского, 66А",
                "addressId": 22319477,
                "districtId": 123,
                "geoFieldsHash": _FAKE_JWT,
                "latitude": 56.840872,
                "longitude": 60.654077,
                "locationId": 654070,
                "metroId": None,
            }
        ),
    }
    getdata_c = {
        "status": 200,
        "body": json.dumps(
            {
                "price": {
                    "recommendedPrice": 7370000,
                    "lowerPrice": 7148900,
                    "higherPrice": 7738500,
                    "count": 800,
                },
                "placementHistory": {"items": []},
                "suggestions": {"items": [], "searchSimilarUrl": "/ekb/kvartiry"},
            }
        ),
    }
    return [warmup, coords_a, position_b, getdata_c]


def test_evaluate_via_imv_browser_happy_path() -> None:
    bf = AsyncMock()
    bf.fetch_json = AsyncMock(side_effect=_canned_responses())

    evaluation = asyncio.run(
        evaluate_via_imv(
            address="Свердловская область, Екатеринбург, ул. Чайковского, 66А",
            rooms=2,
            area_m2=54.0,
            floor=4,
            floor_at_home=9,
            house_type="panel",
            renovation_type="cosmetic",
            has_balcony=True,
            has_loggia=False,
            browser_fetcher=bf,
        )
    )

    assert evaluation.recommended_price == 7370000
    assert evaluation.lower_price == 7148900
    assert evaluation.higher_price == 7738500
    assert evaluation.market_count == 800
    assert evaluation.geo.geo_hash == _FAKE_JWT
    assert evaluation.geo.avito_address_id == 22319477
    assert evaluation.geo.avito_location_id == 654070
    assert evaluation.geo.lat == 56.840872
    assert len(evaluation.cache_key) == 64  # sha256 hex

    # 4 вызова fetch_json: warm-up, coords, position, get-data.
    assert bf.fetch_json.await_count == 4
    # Каждый ходил с origin=WARMUP_URL (same-origin якорь avito.ru).
    for call in bf.fetch_json.await_args_list:
        assert call.kwargs["origin"] == WARMUP_URL
    # Последний (get-data) — POST на правильный endpoint.
    last_call = bf.fetch_json.await_args_list[-1]
    assert last_call.args[0] == f"{AVITO_BASE}/web/1/realty-imv/get-data"
    assert last_call.kwargs["method"] == "POST"
    # body несёт geoHash из step B.
    sent_body: dict[str, Any] = json.loads(last_call.kwargs["body"])
    assert sent_body["geoHash"] == _FAKE_JWT
    assert sent_body["rooms"] == 2


def test_evaluate_via_imv_browser_403_on_getdata_raises_auth() -> None:
    responses = _canned_responses()
    responses[-1] = {"status": 403, "body": "forbidden"}  # get-data → 403
    bf = AsyncMock()
    bf.fetch_json = AsyncMock(side_effect=responses)

    with pytest.raises(IMVAuthError):
        asyncio.run(
            evaluate_via_imv(
                address="ЕКБ, ул. Чайковского, 66А",
                rooms=2,
                area_m2=54.0,
                floor=4,
                floor_at_home=9,
                house_type="panel",
                renovation_type="cosmetic",
                has_balcony=True,
                has_loggia=False,
                browser_fetcher=bf,
            )
        )
