"""Тесты для domrf_catalog_object.py (issue #297 sub-task 22d).

Покрывает:
  - extract_next_data — парсинг HTML с __NEXT_DATA__
  - parse_catalog_object — маппинг pageProps → DB columns
  - value helpers (_to_numeric_comma, _to_bool_da_net, _to_date_ddmmyyyy)
  - partial responses (partial pageProps → all other fields = None, no crash)
"""

from __future__ import annotations

from datetime import date
from typing import Any

import pytest

from app.services.scrapers.domrf_catalog_object import (
    _to_bool_da_net,
    _to_bool_int,
    _to_date_ddmmyyyy,
    _to_numeric_comma,
    extract_next_data,
    parse_catalog_object,
)

# ── extract_next_data ─────────────────────────────────────────────────────────


def test_extract_next_data_from_html() -> None:
    """Базовый case: тег найден, JSON возвращается как dict."""
    html = (
        "<html><head>"
        '<script id="__NEXT_DATA__" type="application/json">'
        '{"props":{"pageProps":{"buildingClass":"Комфорт"}}}'
        "</script>"
        "</head></html>"
    )
    result = extract_next_data(html)
    assert isinstance(result, dict)
    assert result["props"]["pageProps"]["buildingClass"] == "Комфорт"


def test_extract_next_data_single_quotes() -> None:
    """Тег с одинарными кавычками тоже должен парситься."""
    html = "<script id='__NEXT_DATA__'>" '{"props":{"pageProps":{}}}' "</script>"
    result = extract_next_data(html)
    assert "props" in result


def test_extract_next_data_not_found_raises() -> None:
    """Если тег не найден — ValueError."""
    with pytest.raises(ValueError, match="__NEXT_DATA__"):
        extract_next_data("<html><body>no script here</body></html>")


def test_extract_next_data_invalid_json_raises() -> None:
    """Если JSON некорректный — ValueError."""
    html = '<script id="__NEXT_DATA__">{broken json</script>'
    with pytest.raises(ValueError):
        extract_next_data(html)


# ── parse_catalog_object — full sample ───────────────────────────────────────


def _make_full_next_data() -> dict[str, Any]:
    """Реалистичный full next_data для obj_id=65136 (подтверждён live)."""
    return {
        "props": {
            "pageProps": {
                "buildingClass": "Комфорт",
                "wallMaterial": "Монолит-кирпич",
                "objEnergyEfficiency": "B",
                "parkingCount": 246,
                "finishTypeCount": 1,
                "freePlan": "Нет",
                "publicationDate": "31.03.2025",
                "additionalInfo": {
                    "objectParkingPlaces": 43,
                    "nearbyParkingPlaces": 0,
                    "ceilingHeight": "2,7",
                    "passengerElevatorsCount": 0,
                    "cargoElevatorsCount": 0,
                    "cargoPassengerElevatorCount": 4,
                    "playgroundsCount": 6,
                    "sportsgroundCount": 5,
                    "bicycleLane": 0,
                    "trashAreaCount": 3,
                    "ramp": 0,
                    "curbLowering": 1,
                    "wheelchairElevatorsCount": 0,
                    "parkingAvailabilityPerc": 60,
                },
                "quartography": {
                    "objLivElemEntrCnt": 1,
                    "objLivElemSqAvg": 46.2,
                    "nonLivFirstFloor": 1,
                },
                "indexes": {
                    "infrastructure": 10,
                    "transport": 6,
                },
                "projectDeclaration": {
                    "number": "66-001686",
                },
            }
        }
    }


def test_parse_catalog_object_full() -> None:
    """Полный sample: все 25+ полей должны быть замаплены корректно."""
    data = parse_catalog_object(_make_full_next_data())

    assert data["obj_class"] == "Комфорт"
    assert data["wall_type"] == "Монолит-кирпич"
    assert data["energy_eff"] == "B"
    assert data["section_count"] == 1
    assert data["parking_total_slots"] == 246
    assert data["guest_parking_inside_count"] == 43
    assert data["guest_parking_outside_count"] == 0
    assert data["ceiling_height_m"] == pytest.approx(2.7)
    assert data["finishing_variants_count"] == 1
    assert data["has_free_planning"] is False
    assert data["avg_flat_area_m2"] == pytest.approx(46.2)
    assert data["elevators_passenger_count"] == 0
    assert data["elevators_cargo_count"] == 4  # 0 + 4
    assert data["playground_kids_count"] == 6
    assert data["playground_sport_count"] == 5
    assert data["has_bike_paths"] is False  # bicycleLane=0
    assert data["trash_areas_count"] == 3
    assert data["has_ramp"] is False  # ramp=0
    assert data["has_low_platforms"] is True  # curbLowering=1
    assert data["has_wheelchair_lift"] is False  # wheelchairElevatorsCount=0
    assert data["first_floor_type"] == "нежилой"  # nonLivFirstFloor=1
    assert data["parking_provision_pct"] == 60
    assert data["project_published_at"] == date(2025, 3, 31)
    assert data["project_declaration_num"] == "66-001686"
    assert data["domrf_score_infrastructure"] == 10
    assert data["domrf_score_transport"] == 6


def test_parse_catalog_object_has_free_planning_da() -> None:
    """freePlan='Да' → has_free_planning=True."""
    nd: dict[str, Any] = {"props": {"pageProps": {"freePlan": "Да"}}}
    data = parse_catalog_object(nd)
    assert data["has_free_planning"] is True


def test_parse_catalog_object_first_floor_zhiloj() -> None:
    """nonLivFirstFloor=0 → first_floor_type='жилой'."""
    nd: dict[str, Any] = {
        "props": {
            "pageProps": {
                "quartography": {"nonLivFirstFloor": 0},
            }
        }
    }
    data = parse_catalog_object(nd)
    assert data["first_floor_type"] == "жилой"


def test_parse_catalog_object_elevators_cargo_sum() -> None:
    """elevators_cargo_count = cargoElevatorsCount + cargoPassengerElevatorCount."""
    nd: dict[str, Any] = {
        "props": {
            "pageProps": {
                "additionalInfo": {
                    "cargoElevatorsCount": 2,
                    "cargoPassengerElevatorCount": 3,
                }
            }
        }
    }
    data = parse_catalog_object(nd)
    assert data["elevators_cargo_count"] == 5


def test_parse_catalog_object_partial() -> None:
    """Только buildingClass → остальные поля None, без исключений."""
    nd: dict[str, Any] = {"props": {"pageProps": {"buildingClass": "Бизнес"}}}
    data = parse_catalog_object(nd)
    assert data["obj_class"] == "Бизнес"
    assert data["wall_type"] is None
    assert data["energy_eff"] is None
    assert data["section_count"] is None
    assert data["parking_total_slots"] is None
    assert data["ceiling_height_m"] is None
    assert data["has_free_planning"] is None
    assert data["elevators_cargo_count"] is None
    assert data["project_published_at"] is None
    assert data["domrf_score_infrastructure"] is None


def test_parse_catalog_object_empty() -> None:
    """Полностью пустой next_data → все поля None, без исключений."""
    data = parse_catalog_object({})
    for v in data.values():
        assert v is None


def test_parking_provision_pct_preserves_float() -> None:
    """parking_provision_pct should preserve fractional values (column is numeric(5,1))."""
    next_data: dict[str, Any] = {
        "props": {"pageProps": {"id": 65136, "additionalInfo": {"parkingAvailabilityPerc": 60.5}}}
    }
    result = parse_catalog_object(next_data)
    assert result["parking_provision_pct"] == 60.5


# ── _to_numeric_comma ─────────────────────────────────────────────────────────


@pytest.mark.parametrize(
    "inp,expected",
    [
        ("2,7", 2.7),
        ("2.7", 2.7),
        ("3,50", 3.5),
        ("", None),
        (None, None),
        ("   ", None),
        ("abc", None),
    ],
)
def test_to_numeric_comma(inp: Any, expected: float | None) -> None:
    result = _to_numeric_comma(inp)
    if expected is None:
        assert result is None
    else:
        assert result == pytest.approx(expected)


# ── _to_bool_da_net ───────────────────────────────────────────────────────────


@pytest.mark.parametrize(
    "inp,expected",
    [
        ("Да", True),
        ("да", True),
        ("ДА", True),
        ("Нет", False),
        ("нет", False),
        ("НЕТ", False),
        ("", None),
        (None, None),
        ("maybe", None),
        ("Yes", None),
    ],
)
def test_to_bool_da_net(inp: Any, expected: bool | None) -> None:
    assert _to_bool_da_net(inp) == expected


# ── _to_bool_int ──────────────────────────────────────────────────────────────


@pytest.mark.parametrize(
    "inp,expected",
    [
        (0, False),
        (1, True),
        (5, True),
        ("1", True),
        ("0", False),
        (None, None),
    ],
)
def test_to_bool_int(inp: Any, expected: bool | None) -> None:
    assert _to_bool_int(inp) == expected


# ── _to_date_ddmmyyyy ─────────────────────────────────────────────────────────


@pytest.mark.parametrize(
    "inp,expected",
    [
        ("31.03.2025", date(2025, 3, 31)),
        ("01.01.2024", date(2024, 1, 1)),
        ("", None),
        (None, None),
        ("2025-03-31", None),  # неправильный формат → None
        ("abc", None),
        ("31.13.2025", None),  # невалидный месяц → None
    ],
)
def test_to_date_ddmmyyyy(inp: Any, expected: date | None) -> None:
    assert _to_date_ddmmyyyy(inp) == expected
