"""#2002: premium_building флаг + класс из curated overlay → MV `premium_houses`.

Покрывает read-only helper `_is_premium_building`, который возвращает 3-кортеж
`(is_premium, med_ppm2, building_class)` из двухслойного overlay:
  1. `premium_buildings_curated` (data/sql/142) — AI/human-выверенный слой (класс +
     is_premium с false-positive demotions); ПЕРЕБИВАЕТ MV;
  2. `premium_houses` MV (data/sql/139) — даёт med_ppm2 + fallback-членство.

Сценарии:
  - curated premium=True с классом → (True, MV.med_ppm2, tier);
  - curated premium=False демотит дом из MV → (False, med, tier) — false-positive;
  - невыверенный дом в MV → (True, med, None) — членство есть, класс неизвестен;
  - дом нигде → (False, None, None);
  - house_id is None → (False, None, None) без обращения к БД;
  - таблица отсутствует / SQL-ошибка → graceful (False, None, None), оценка не падает.

Плюс smoke-проверка дефолтов в response-модели AggregatedEstimate (флаг — метаданные,
сериализация без изменений, когда дом не премиальный).
"""

from __future__ import annotations

import os
from typing import Any
from unittest.mock import MagicMock

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

import pytest

from app.services.estimator import _is_premium_building


def _make_mock_db(
    *, curated_row: Any = None, mv_row: Any = None, raises: bool = False
) -> MagicMock:
    """mock db.execute, маршрутизируя по SQL-тексту.

    `SELECT ... FROM premium_buildings_curated ...` → curated_row;
    `SELECT med_ppm2 FROM premium_houses ...`       → mv_row.
    raises=True имитирует отсутствие таблицы (первый же execute бросает).
    """
    mock_db = MagicMock()
    if raises:
        mock_db.execute.side_effect = RuntimeError(
            'relation "premium_buildings_curated" does not exist'
        )
        return mock_db

    def _execute(sql: Any, params: Any = None) -> MagicMock:
        text_sql = str(getattr(sql, "text", sql))
        result = MagicMock()
        if "premium_buildings_curated" in text_sql:
            result.first.return_value = curated_row
        elif "premium_houses" in text_sql:
            result.first.return_value = mv_row
        else:  # pragma: no cover — defensive
            result.first.return_value = None
        return result

    mock_db.execute.side_effect = _execute
    return mock_db


def _executed_sql(db: MagicMock) -> list[str]:
    """Тексты всех SQL-запросов, прошедших через db.execute."""
    return [str(call.args[0].text) for call in db.execute.call_args_list]


def test_curated_premium_with_class_overrides_mv() -> None:
    """curated is_premium=True + tier → (True, MV.med_ppm2, tier); класс из curated."""
    db = _make_mock_db(curated_row=(True, "премиум"), mv_row=(422_000,))

    assert _is_premium_building(db, 11370) == (True, 422_000, "премиум")
    # обе таблицы используют CAST(:hid AS bigint) — psycopg v3, не :hid::bigint
    sqls = _executed_sql(db)
    assert any("premium_buildings_curated" in s for s in sqls)
    assert any("premium_houses" in s for s in sqls)
    for s in sqls:
        assert "CAST(:hid AS bigint)" in s
        assert "::bigint" not in s


def test_curated_false_positive_demotes_mv_member() -> None:
    """curated is_premium=False демотит дом-член MV → (False, med, tier).

    Концентрационный MV пометил дом премиальным, но curation признал его
    false-positive (mass-market). curated перебивает — флаг False, но med и класс
    из данных сохраняются как контекст для ревьюера.
    """
    db = _make_mock_db(curated_row=(False, "комфорт"), mv_row=(211_000,))

    assert _is_premium_building(db, 9401) == (False, 211_000, "комфорт")


def test_curated_class_flows_when_mv_absent() -> None:
    """curated есть, дома нет в MV → (curated.is_premium, None, tier)."""
    db = _make_mock_db(curated_row=(True, "бизнес"), mv_row=None)

    assert _is_premium_building(db, 6685) == (True, None, "бизнес")


def test_uncurated_mv_member_premium_true_class_none() -> None:
    """Невыверенный дом, но член MV → (True, med, None) — класс неизвестен."""
    db = _make_mock_db(curated_row=None, mv_row=(255_000,))

    assert _is_premium_building(db, 4242) == (True, 255_000, None)


def test_not_premium_anywhere_yields_false_none_none() -> None:
    """Дом отсутствует и в curated, и в MV → (False, None, None)."""
    db = _make_mock_db(curated_row=None, mv_row=None)

    assert _is_premium_building(db, 9999) == (False, None, None)


def test_house_id_none_short_circuits_without_query() -> None:
    """house_id is None → (False, None, None), к БД не обращаемся."""
    db = _make_mock_db(curated_row=(True, "премиум"), mv_row=(255_000,))

    assert _is_premium_building(db, None) == (False, None, None)
    db.execute.assert_not_called()


def test_missing_table_degrades_gracefully() -> None:
    """Таблица не существует / SQL-ошибка → (False, None, None), без проброса."""
    db = _make_mock_db(raises=True)

    assert _is_premium_building(db, 4242) == (False, None, None)
    # после ошибки откатываем poisoned-транзакцию
    db.rollback.assert_called_once()


def test_mv_median_null_is_handled() -> None:
    """Дом в MV, но med_ppm2 NULL → median None (флаг/класс не теряются)."""
    db = _make_mock_db(curated_row=(True, "элит"), mv_row=(None,))

    assert _is_premium_building(db, 7109) == (True, None, "элит")


def test_response_model_defaults_non_premium() -> None:
    """AggregatedEstimate по умолчанию: premium_building=False, median/class=None.

    Гарантирует, что добавление полей не меняет сериализацию для не-премиум оценок.
    """
    from datetime import UTC, datetime
    from uuid import uuid4

    from app.schemas.trade_in import AggregatedEstimate

    est = AggregatedEstimate(
        estimate_id=uuid4(),
        median_price_rub=10_000_000,
        range_low_rub=9_000_000,
        range_high_rub=11_000_000,
        median_price_per_m2=200_000,
        confidence="medium",
        n_analogs=12,
        period_months=24,
        analogs=[],
        actual_deals=[],
        expires_at=datetime.now(tz=UTC),
    )

    assert est.premium_building is False
    assert est.premium_building_median_ppm2 is None
    assert est.premium_building_class is None
    dumped = est.model_dump()
    assert dumped["premium_building"] is False
    assert dumped["premium_building_median_ppm2"] is None
    assert dumped["premium_building_class"] is None


def test_response_model_carries_curated_class() -> None:
    """premium_building_class сериализуется, когда задан (curated overlay)."""
    from datetime import UTC, datetime
    from uuid import uuid4

    from app.schemas.trade_in import AggregatedEstimate

    est = AggregatedEstimate(
        estimate_id=uuid4(),
        median_price_rub=42_000_000,
        range_low_rub=40_000_000,
        range_high_rub=44_000_000,
        median_price_per_m2=420_000,
        confidence="medium",
        n_analogs=8,
        period_months=24,
        analogs=[],
        actual_deals=[],
        expires_at=datetime.now(tz=UTC),
        premium_building=True,
        premium_building_median_ppm2=422_000,
        premium_building_class="премиум",
    )

    assert est.model_dump()["premium_building_class"] == "премиум"


if __name__ == "__main__":  # pragma: no cover
    raise SystemExit(pytest.main([__file__, "-q"]))
