"""Тесты ЖКХ-loader'а знаменателя (app/services/zhkh_flats_loader.py, мигр. 146/149).

Только ЧИСТЫЕ хелперы — БЕЗ сети и БД:
  - parse_int — числовой парс полей ГИС ЖКХ.
  - _build_body — полный DTO searchByAddress (15 ключей; pageIndex — query-param, НЕ в теле).
  - _parse_item — извлечение guid + полей из реалистичного item ответа.
  - статические asserts по _UPDATE_SQL (psycopg v3 CAST, IS DISTINCT FROM gate).
"""

from __future__ import annotations

import os
import re

# settings/SessionLocal не импортируем, но пакет app.* на импорте может тронуть DSN — как в
# sibling-тестах задаём заглушку (ядро статично/без БД).
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

from app.services import zhkh_flats_loader as zfl


# ─────────────────────────────────────────────────────────────────────────────
# parse_int
# ─────────────────────────────────────────────────────────────────────────────
def test_parse_int_robust() -> None:
    assert zfl.parse_int("429") == 429
    assert zfl.parse_int("0") is None  # 0 квартир — не пригодный знаменатель
    assert zfl.parse_int("") is None
    assert zfl.parse_int(None) is None
    assert zfl.parse_int(162) == 162
    assert zfl.parse_int("x") is None


# ─────────────────────────────────────────────────────────────────────────────
# _build_body — полный DTO
# ─────────────────────────────────────────────────────────────────────────────
def test_build_body_has_all_dto_keys_and_batch() -> None:
    batch = ["g1", "g2", "g3"]
    body = zfl._build_body(batch)
    expected_keys = {
        "regionCode",
        "fiasHouseCodeList",
        "estStatus",
        "strStatus",
        "calcCount",
        "houseConditionRefList",
        "houseTypeRefList",
        "houseManagementTypeRefList",
        "cadastreNumber",
        "oktmo",
        "statuses",
        "regionProperty",
        "municipalProperty",
        "hostelTypeCodes",
        "useReadOnlyDataSource",
    }
    assert len(expected_keys) == 15
    assert set(body.keys()) == expected_keys
    assert body["fiasHouseCodeList"] == batch
    assert body["statuses"] == ["APPROVED"]
    assert body["regionCode"] == zfl.REGION_GUID
    assert body["calcCount"] is True
    assert body["useReadOnlyDataSource"] is True
    # pageIndex — query-параметр в ENDPOINT, его НЕТ в теле.
    assert "pageIndex" not in body
    assert "pageIndex=1" in zfl.ENDPOINT
    assert "elementsPerPage=100" in zfl.ENDPOINT


# ─────────────────────────────────────────────────────────────────────────────
# _body_by_cadastre — тот же DTO, но поиск по кадастру
# ─────────────────────────────────────────────────────────────────────────────
def test_body_by_cadastre_sets_cadastre_and_nulls_fias() -> None:
    body = zfl._body_by_cadastre("66:41:0604007:747")
    expected_keys = {
        "regionCode",
        "fiasHouseCodeList",
        "estStatus",
        "strStatus",
        "calcCount",
        "houseConditionRefList",
        "houseTypeRefList",
        "houseManagementTypeRefList",
        "cadastreNumber",
        "oktmo",
        "statuses",
        "regionProperty",
        "municipalProperty",
        "hostelTypeCodes",
        "useReadOnlyDataSource",
    }
    assert len(expected_keys) == 15
    assert set(body.keys()) == expected_keys
    # cadastreNumber — ОДИНОЧНОЕ значение (НЕ список); fiasHouseCodeList = None.
    assert body["cadastreNumber"] == "66:41:0604007:747"
    assert body["fiasHouseCodeList"] is None
    assert body["regionCode"] == zfl.REGION_GUID
    assert body["statuses"] == ["APPROVED"]
    assert body["useReadOnlyDataSource"] is True


# ─────────────────────────────────────────────────────────────────────────────
# _parse_floors_max — макс. этаж из сырой maxFloorCount
# ─────────────────────────────────────────────────────────────────────────────
def test_parse_floors_max() -> None:
    assert zfl._parse_floors_max("10-30") == 30
    assert zfl._parse_floors_max("17,23") == 23
    assert zfl._parse_floors_max("9") == 9
    assert zfl._parse_floors_max(None) is None
    assert zfl._parse_floors_max("") is None
    assert zfl._parse_floors_max("нет") is None


# ─────────────────────────────────────────────────────────────────────────────
# _parse_item
# ─────────────────────────────────────────────────────────────────────────────
def test_parse_item_realistic() -> None:
    item = {
        "residentialPremiseCount": "429",
        "operationYear": 2021,
        "buildingYear": "2019",
        "maxFloorCount": "10-30",
        "address": {"house": {"houseGuid": "abc-guid-123"}},
        "houseType": {"houseTypeName": "Многоквартирный"},
    }
    parsed = zfl._parse_item(item)
    assert parsed is not None
    assert parsed.house_guid == "abc-guid-123"
    assert parsed.flat_count == 429
    assert parsed.year == 2021  # operationYear предпочтительнее buildingYear
    assert parsed.floors_raw == "10-30"  # сырая строка, не распарсенный максимум


def test_parse_item_falls_back_to_building_year() -> None:
    item = {
        "residentialPremiseCount": "100",
        "operationYear": None,
        "buildingYear": "1998",
        "maxFloorCount": "9",
        "address": {"house": {"houseGuid": "g-fallback"}},
    }
    parsed = zfl._parse_item(item)
    assert parsed is not None
    assert parsed.year == 1998
    assert parsed.flat_count == 100


def test_parse_item_no_guid_returns_none() -> None:
    assert zfl._parse_item({"residentialPremiseCount": "100"}) is None
    assert zfl._parse_item({"address": {"house": {}}}) is None
    assert zfl._parse_item({"address": {}}) is None


def test_parse_item_zero_flatcount_is_none() -> None:
    item = {
        "residentialPremiseCount": "0",
        "maxFloorCount": None,
        "address": {"house": {"houseGuid": "g-zero"}},
    }
    parsed = zfl._parse_item(item)
    assert parsed is not None
    assert parsed.flat_count is None  # «0» → None (не пригодный знаменатель)
    assert parsed.floors_raw is None
    assert parsed.year is None


# ─────────────────────────────────────────────────────────────────────────────
# Статические asserts по _UPDATE_SQL (psycopg v3, идемпотентность)
# ─────────────────────────────────────────────────────────────────────────────
def test_update_sql_uses_psycopg_v3_cast_not_double_colon() -> None:
    sql = str(zfl._UPDATE_SQL.text)
    # Никаких :name::type — только CAST(:name AS type).
    assert not re.search(r":\w+::", sql)
    assert "CAST(:fc AS int)" in sql
    assert "CAST(:g AS text)" in sql
    assert "CAST(:yr AS smallint)" in sql
    assert "CAST(:fr AS text)" in sql


def test_update_sql_is_idempotent_gate() -> None:
    flat = re.sub(r"\s+", " ", str(zfl._UPDATE_SQL.text))
    assert "UPDATE houses" in flat
    assert "WHERE gar_house_guid = CAST(:g AS text)" in flat
    # Повторный прогон не трогает уже совпавшие строки (IS DISTINCT FROM gate).
    assert "IS DISTINCT FROM CAST(:fc AS int)" in flat
    assert "zhkh_matched_at = now()" in flat
