"""Tests for Fix 3: yandex history items с area_m2 <= 0 или None отброшены при сохранении.

_save_yandex_history_items должен:
- пропустить item если area_m2 is None или area_m2 <= 0
- сохранить валидные items (area_m2 > 0) без потерь
- не ронять батч из-за одного битого item
- лого кол-во отброшенных items

Легаси `app.services.scrapers.yandex_valuation` удалён (#2277 финальный шаг
scraper_kit-миграции) — `result` типы переведены на kit-эквивалент
`scraper_kit.providers.yandex.valuation`.
"""

from __future__ import annotations

import os
from datetime import date
from unittest.mock import MagicMock, patch

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

from scraper_kit.providers.yandex.valuation import (
    ValuationHistoryItem,
    ValuationHouseMeta,
    YandexValuationResult,
)

from app.services.estimator import _save_yandex_history_items


def _make_result(items: list[ValuationHistoryItem]) -> YandexValuationResult:
    return YandexValuationResult(
        address="Россия, Свердловская область, Екатеринбург, ул. Куйбышева, 106",
        offer_category="APARTMENT",
        offer_type="SELL",
        page=1,
        source_url="https://realty.yandex.ru/otsenka-kvartiry-po-adresu-onlayn/?address=test",
        house=ValuationHouseMeta(year_built=2010, total_floors=16),
        history_items=items,
    )


def _valid_item(area_m2: float = 50.0) -> ValuationHistoryItem:
    return ValuationHistoryItem(
        area_m2=area_m2,
        rooms=2,
        floor=5,
        start_price=9_000_000,
        last_price=8_800_000,
        publish_date=date(2024, 4, 10),
        exposure_days=30,
        status="Снято",
    )


def _item_with_area(area_m2: float | None) -> ValuationHistoryItem:
    return ValuationHistoryItem(
        area_m2=area_m2,
        rooms=1,
        floor=2,
        start_price=6_000_000,
        last_price=6_000_000,
        publish_date=date(2024, 1, 1),
        exposure_days=10,
    )


# ── тест 1: item с area=None отброшен ────────────────────────────────────────


def test_item_with_area_none_is_skipped() -> None:
    """Item с area_m2=None должен быть пропущен, валидный item сохранён."""
    items = [
        _item_with_area(None),  # битый — пропускаем
        _valid_item(50.0),  # валидный — сохраняем
    ]
    db = MagicMock()
    result = _make_result(items)

    with patch(
        "app.services.estimator.match_or_create_house",
        return_value=(99, 0.9, "fp"),
    ):
        saved = _save_yandex_history_items(db, result)

    assert saved == 1, f"Ожидали 1 сохранённый item, получили {saved}"
    rows = db.execute.call_args_list[0].args[1]
    assert len(rows) == 1
    assert rows[0]["area"] == 50.0


# ── тест 2: item с area=0 отброшен ───────────────────────────────────────────


def test_item_with_area_zero_is_skipped() -> None:
    """Item с area_m2=0 должен быть пропущен."""
    items = [
        _item_with_area(0.0),  # area=0 — пропускаем
        _item_with_area(0),  # area=0 (int) — пропускаем
        _valid_item(55.0),  # валидный
    ]
    db = MagicMock()
    result = _make_result(items)

    with patch(
        "app.services.estimator.match_or_create_house",
        return_value=(1, 1.0, "new"),
    ):
        saved = _save_yandex_history_items(db, result)

    assert saved == 1
    rows = db.execute.call_args_list[0].args[1]
    assert rows[0]["area"] == 55.0


# ── тест 3: item с area < 0 отброшен ─────────────────────────────────────────


def test_item_with_negative_area_is_skipped() -> None:
    """Item с area_m2 < 0 должен быть пропущен."""
    items = [
        _item_with_area(-5.0),  # отрицательная площадь
        _valid_item(42.0),
    ]
    db = MagicMock()
    result = _make_result(items)

    with patch(
        "app.services.estimator.match_or_create_house",
        return_value=(2, 0.7, "geo"),
    ):
        saved = _save_yandex_history_items(db, result)

    assert saved == 1


# ── тест 4: все битые → 0 сохранено, но батч не падает ───────────────────────


def test_all_invalid_area_returns_zero_no_crash() -> None:
    """Все items с area=None или 0 → saved=0, ни одного db.execute INSERT-вызова с данными."""
    items = [
        _item_with_area(None),
        _item_with_area(0.0),
        _item_with_area(None),
    ]
    db = MagicMock()
    result = _make_result(items)

    with patch(
        "app.services.estimator.match_or_create_house",
        return_value=(3, 0.9, "fp"),
    ):
        saved = _save_yandex_history_items(db, result)

    assert saved == 0
    # db.execute не должен вызываться для пустого rows (нет INSERT)
    # Commit вызывается, rollback — нет.
    db.commit.assert_called_once()
    db.rollback.assert_not_called()


# ── тест 5: все валидные — всё сохранено ─────────────────────────────────────


def test_all_valid_items_all_saved() -> None:
    """Все items с корректной площадью сохраняются без потерь."""
    items = [_valid_item(float(40 + i * 5)) for i in range(5)]
    db = MagicMock()
    result = _make_result(items)

    with patch(
        "app.services.estimator.match_or_create_house",
        return_value=(10, 0.9, "fp"),
    ):
        saved = _save_yandex_history_items(db, result)

    assert saved == 5


# ── тест 6: микс валидных и битых ─────────────────────────────────────────────


def test_mixed_items_only_valid_saved() -> None:
    """Микс: 2 валидных, 3 битых → saved=2."""
    items = [
        _valid_item(40.0),
        _item_with_area(None),
        _valid_item(60.0),
        _item_with_area(0.0),
        _item_with_area(-1.0),
    ]
    db = MagicMock()
    result = _make_result(items)

    with patch(
        "app.services.estimator.match_or_create_house",
        return_value=(20, 0.9, "fp"),
    ):
        saved = _save_yandex_history_items(db, result)

    assert saved == 2
    rows = db.execute.call_args_list[0].args[1]
    assert len(rows) == 2
    areas = {r["area"] for r in rows}
    assert areas == {40.0, 60.0}
