"""Offline smoke tests для scraper_kit avito IMV провайдера.

Без сети — только cache_key вычисление, парсинг заголовков, unix→date, dataclass
конструирование. Легаси `app.services.scrapers.avito_imv` удалён (#2277 финальный шаг
scraper_kit-миграции, 0 runtime importers) — тесты переведены на kit-эквивалент
`scraper_kit.providers.avito.imv`, регрессия по контракту step B/C (issue #565)
сохранена без изменений.
"""

import json
from datetime import date
from pathlib import Path

import pytest
from scraper_kit.providers.avito.imv import (
    IMVAddressNotFoundError,
    IMVEvaluation,
    IMVGeo,
    IMVPlacementHistoryItem,
    _parse_geo_position,
    _parse_placement_history,
    _parse_placement_item,
    _parse_price,
    _parse_suggestion,
    _parse_suggestions,
    _parse_title,
    _unix_to_date,
    compute_imv_cache_key,
)

_FIXTURES = Path(__file__).parent / "fixtures"


def _load_fixture(name: str) -> dict:
    with (_FIXTURES / name).open(encoding="utf-8") as f:
        return json.load(f)


# ---------------------------------------------------------------------------
# cache_key
# ---------------------------------------------------------------------------


def test_cache_key_deterministic():
    k1 = compute_imv_cache_key(
        "ЕКБ ул. Чайковского, 66А", 2, 42.0, 4, 5, "panel", "cosmetic", True, False
    )
    k2 = compute_imv_cache_key(
        "ЕКБ ул. Чайковского, 66А", 2, 42.0, 4, 5, "panel", "cosmetic", True, False
    )
    assert k1 == k2
    assert len(k1) == 64  # sha256 hex


def test_cache_key_differs_by_rooms():
    k1 = compute_imv_cache_key(
        "ЕКБ ул. Чайковского, 66А", 2, 42.0, 4, 5, "panel", "cosmetic", True, False
    )
    k3 = compute_imv_cache_key(
        "ЕКБ ул. Чайковского, 66А", 3, 42.0, 4, 5, "panel", "cosmetic", True, False
    )
    assert k1 != k3


def test_cache_key_differs_by_balcony():
    k_with = compute_imv_cache_key("ул. Ленина, 1", 1, 30.0, 1, 9, "brick", "euro", True, False)
    k_without = compute_imv_cache_key("ул. Ленина, 1", 1, 30.0, 1, 9, "brick", "euro", False, False)
    assert k_with != k_without


def test_cache_key_differs_by_address():
    k1 = compute_imv_cache_key("ул. А, 1", 2, 42.0, 4, 5, "panel", "cosmetic", True, False)
    k2 = compute_imv_cache_key("ул. Б, 2", 2, 42.0, 4, 5, "panel", "cosmetic", True, False)
    assert k1 != k2


# ---------------------------------------------------------------------------
# _unix_to_date
# ---------------------------------------------------------------------------


def test_unix_date_none():
    assert _unix_to_date(None) is None


def test_unix_date_zero():
    assert _unix_to_date(0) is None


def test_unix_date_positive():
    result = _unix_to_date(1715000000)
    assert isinstance(result, date)
    # 1715000000 примерно 2024-05-06 UTC+5 (Екатеринбург)
    assert result.year == 2024
    assert result.month in (5, 6)  # зависит от локали


def test_unix_date_float():
    result = _unix_to_date(1697000000.0)
    assert isinstance(result, date)
    assert result.year == 2023


# ---------------------------------------------------------------------------
# _parse_title
# ---------------------------------------------------------------------------


def test_parse_title_full():
    parsed = _parse_title("2-к. квартира, 42 м², 4/5 эт.")
    assert parsed["rooms"] == 2
    assert parsed["area_m2"] == 42.0
    assert parsed["floor"] == 4
    assert parsed["total_floors"] == 5


def test_parse_title_comma_decimal():
    parsed = _parse_title("1-к. квартира, 27,1 м², 2/5 эт.")
    assert parsed["rooms"] == 1
    assert parsed["area_m2"] == pytest.approx(27.1)
    assert parsed["floor"] == 2
    assert parsed["total_floors"] == 5


def test_parse_title_none():
    parsed = _parse_title(None)
    assert parsed["rooms"] is None
    assert parsed["area_m2"] is None


def test_parse_title_no_match():
    parsed = _parse_title("студия, 20 м²")
    assert parsed["rooms"] is None


# ---------------------------------------------------------------------------
# _parse_placement_item
# ---------------------------------------------------------------------------


def test_parse_placement_item_full():
    raw = {
        "id": 9876543210,
        "title": "2-к. квартира, 42 м², 4/5 эт.",
        "startPrice": 6500000,
        "startPriceDate": 1697000000,
        "lastPrice": 6290000,
        "lastPriceDate": 1715000000,
        "removedDate": 1715500000,
        "exposure": 210,
        "itemImage": {"url": "https://avito.ru/img.jpg"},  # должен быть отфильтрован
    }
    item = _parse_placement_item(raw)

    assert item.ext_item_id == "9876543210"
    assert item.title == "2-к. квартира, 42 м², 4/5 эт."
    assert item.rooms == 2
    assert item.area_m2 == 42.0
    assert item.floor == 4
    assert item.total_floors == 5
    assert item.start_price == 6500000
    assert isinstance(item.start_price_date, date)
    assert item.last_price == 6290000
    assert isinstance(item.last_price_date, date)
    assert isinstance(item.removed_date, date)
    assert item.exposure_days == 210
    # itemImage должен быть отфильтрован из raw_payload
    assert item.raw_payload is not None
    assert "itemImage" not in item.raw_payload


def test_parse_placement_item_no_removed_date():
    raw = {
        "id": 111,
        "title": "1-к. квартира, 30 м², 2/9 эт.",
        "startPrice": 3000000,
        "startPriceDate": 1697000000,
        "lastPrice": 2900000,
        "lastPriceDate": 1715000000,
        "exposure": 50,
    }
    item = _parse_placement_item(raw)
    assert item.removed_date is None
    assert item.ext_item_id == "111"


# ---------------------------------------------------------------------------
# _parse_suggestion
# ---------------------------------------------------------------------------


def test_parse_suggestion_full():
    raw = {
        "id": 7986882804,
        "title": "2-к. квартира, 42 м², 4/5 эт.",
        "address": "Авиационная улица, 63к2, Екатеринбург",
        "price": 6300000,
        "exposure": 14,
        "publishDate": 1716000000,
        "itemLink": "/ekaterinburg/kvartiry/some-path",
        "metro": {"name": "Чкаловская", "distance": "2,7 км", "colors": ["#cf2734"]},
        "hasGoodPriceBadge": False,
        "imageLink": "https://avito.ru/img.jpg",  # должен быть отфильтрован
    }
    sugg = _parse_suggestion(raw)

    assert sugg.ext_item_id == "7986882804"
    assert sugg.price_rub == 6300000
    assert sugg.metro_name == "Чкаловская"
    assert sugg.metro_distance == "2,7 км"
    assert sugg.metro_color == "#cf2734"
    assert sugg.item_url == "/ekaterinburg/kvartiry/some-path"
    assert sugg.has_good_price_badge is False
    assert isinstance(sugg.publish_date, date)
    # imageLink должен быть отфильтрован
    assert sugg.raw_payload is not None
    assert "imageLink" not in sugg.raw_payload


def test_parse_suggestion_no_metro():
    raw = {
        "id": 999,
        "title": "1-к. квартира, 25 м², 3/5 эт.",
        "price": 4000000,
        "exposure": 5,
        "publishDate": 1716000000,
        "itemLink": "/path",
    }
    sugg = _parse_suggestion(raw)
    assert sugg.metro_name is None
    assert sugg.metro_color is None


# ---------------------------------------------------------------------------
# IMVEvaluation dataclass конструирование
# ---------------------------------------------------------------------------


def test_dataclass_minimal_construct():
    e = IMVEvaluation(
        cache_key="a" * 64,
        address="X",
        rooms=2,
        area_m2=42.0,
        floor=4,
        floor_at_home=5,
        house_type="panel",
        renovation_type="cosmetic",
        has_balcony=True,
        has_loggia=False,
        geo=IMVGeo(geo_hash="JWT.TOKEN.HERE"),
        recommended_price=6_290_000,
        lower_price=6_100_000,
        higher_price=6_600_000,
    )
    assert e.recommended_price == 6_290_000
    assert e.lower_price == 6_100_000
    assert e.higher_price == 6_600_000
    assert e.geo.geo_hash == "JWT.TOKEN.HERE"
    assert e.placement_history == []
    assert e.suggestions == []
    assert e.market_count is None
    assert e.raw_response is None


def test_dataclass_with_history():
    item = IMVPlacementHistoryItem(
        ext_item_id="123",
        title="2-к. квартира, 42 м², 4/5 эт.",
        rooms=2,
        area_m2=42.0,
        floor=4,
        total_floors=5,
        start_price=6500000,
        last_price=6290000,
        removed_date=date(2024, 5, 12),
        exposure_days=210,
    )
    e = IMVEvaluation(
        cache_key="b" * 64,
        address="Y",
        rooms=2,
        area_m2=42.0,
        floor=4,
        floor_at_home=5,
        house_type="panel",
        renovation_type="euro",
        has_balcony=False,
        has_loggia=True,
        geo=IMVGeo(geo_hash="JWT2"),
        recommended_price=6_000_000,
        lower_price=5_800_000,
        higher_price=6_300_000,
        market_count=800,
        placement_history=[item],
    )
    assert len(e.placement_history) == 1
    assert e.placement_history[0].removed_date == date(2024, 5, 12)
    assert e.market_count == 800


# ---------------------------------------------------------------------------
# _parse_geo_position (step B) — driven by live-captured fixture (issue #565)
# ---------------------------------------------------------------------------


def test_parse_geo_position_from_fixture():
    """Real /js/v2/geo/position response (ЕКБ, ул. Малышева 125) → IMVGeo."""
    data_b = _load_fixture("avito_imv_geo_position.json")
    geo = _parse_geo_position(data_b, lat=56.840872, lon=60.654077)

    assert geo.geo_hash  # non-empty JWT
    assert geo.geo_hash.startswith("eyJ")  # base64url JWT header
    assert geo.geo_hash.count(".") == 2  # header.payload.signature
    assert geo.avito_address_id == 22319477
    assert geo.avito_location_id == 654070
    assert geo.avito_district_id == 789
    assert geo.lat == pytest.approx(56.840872)
    assert geo.lon == pytest.approx(60.654077)


def test_parse_geo_position_ekb_has_no_metro():
    """ЕКБ-окраина не отдаёт metroId — должен быть None, не падать."""
    data_b = _load_fixture("avito_imv_geo_position.json")
    geo = _parse_geo_position(data_b, lat=56.84, lon=60.65)
    assert geo.avito_metro_id is None


def test_parse_geo_position_missing_hash_raises():
    """Пустой geoFieldsHash → IMVAddressNotFoundError (graceful skip в estimator)."""
    with pytest.raises(IMVAddressNotFoundError):
        _parse_geo_position(
            {"address": "X", "errorText": "not found", "geoFieldsHash": ""},
            lat=1.0,
            lon=2.0,
        )


# ---------------------------------------------------------------------------
# _parse_price / placement / suggestions (step C) — live-captured fixture
# ---------------------------------------------------------------------------


def test_parse_price_from_fixture():
    """Real realty-imv/get-data price block populates all three prices + count."""
    data = _load_fixture("avito_imv_getdata.json")
    recommended, lower, higher, count = _parse_price(data)

    assert recommended == 7370000
    assert lower == 7148900
    assert higher == 7738500
    assert count == 800
    assert lower < recommended < higher


def test_parse_price_missing_block_defaults_zero():
    """Нет price блока (degenerate ответ) → нули, не KeyError."""
    recommended, lower, higher, count = _parse_price({})
    assert (recommended, lower, higher) == (0, 0, 0)
    assert count is None


def test_parse_placement_history_from_fixture():
    """placementHistory.items парсятся в IMVPlacementHistoryItem с removed_date."""
    data = _load_fixture("avito_imv_getdata.json")
    items = _parse_placement_history(data)

    assert len(items) >= 1
    first = items[0]
    assert first.ext_item_id  # str(id)
    assert first.start_price and first.last_price
    assert isinstance(first.start_price_date, date)
    assert isinstance(first.removed_date, date)
    # raw_payload очищен от тяжёлого itemImage
    assert first.raw_payload is not None
    assert "itemImage" not in first.raw_payload


def test_parse_placement_history_absent_block_returns_empty():
    """placementHistory отсутствует (как у проспект Ленина) → [] без падения."""
    data = _load_fixture("avito_imv_getdata.json")
    data.pop("placementHistory", None)
    assert _parse_placement_history(data) == []


def test_parse_suggestions_from_fixture():
    """suggestions.items парсятся + searchSimilarUrl извлекается."""
    data = _load_fixture("avito_imv_getdata.json")
    suggestions, search_url = _parse_suggestions(data)

    assert len(suggestions) >= 1
    assert all(s.ext_item_id for s in suggestions)
    assert all(s.price_rub for s in suggestions)
    # raw_payload очищен от imageLink
    assert "imageLink" not in (suggestions[0].raw_payload or {})
    assert search_url is not None
    assert search_url.startswith("/")


def test_fixtures_feed_full_evaluation_shape():
    """Сквозной оффлайн-тест: fixtures B+C → заполненный IMVEvaluation.

    Зеркалит то, что _geocode + _imv_evaluate собирают из живого ответа,
    без сети. Регрессионный guard для контракта step B/C (issue #565).
    """
    data_b = _load_fixture("avito_imv_geo_position.json")
    data_c = _load_fixture("avito_imv_getdata.json")

    geo = _parse_geo_position(data_b, lat=56.840872, lon=60.654077)
    recommended, lower, higher, count = _parse_price(data_c)
    history = _parse_placement_history(data_c)
    suggestions, search_url = _parse_suggestions(data_c)

    e = IMVEvaluation(
        cache_key="c" * 64,
        address="Екатеринбург, улица Малышева, 125",
        rooms=2,
        area_m2=54.0,
        floor=4,
        floor_at_home=9,
        house_type="panel",
        renovation_type="cosmetic",
        has_balcony=True,
        has_loggia=False,
        geo=geo,
        recommended_price=recommended,
        lower_price=lower,
        higher_price=higher,
        market_count=count,
        placement_history=history,
        suggestions=suggestions,
        search_similar_url=search_url,
        raw_response=data_c,
    )

    assert e.geo.geo_hash  # non-empty
    assert e.recommended_price == 7370000
    assert e.lower_price == 7148900
    assert e.higher_price == 7738500
    assert e.market_count == 800
    assert len(e.suggestions) >= 1
