"""End-to-end API test — POST /api/v1/concepts returns 3 filled variants.

Goes through the FastAPI route (TestClient) and asserts the contract is populated
with *real* numbers (non-zero ТЭП + financial), valid building GeoJSON, and that a
degenerate parcel yields 422 rather than empty variants.
"""

from __future__ import annotations

from collections.abc import Iterator

import pytest
from fastapi.testclient import TestClient

from app.core.db import get_db
from app.main import app


class _NoDbSession:
    """Заглушка Session: любой execute падает → market-price lookup деградирует в
    class_norm без реальной БД. Тесты API не должны зависеть от живого Postgres."""

    def execute(self, *args: object, **kwargs: object) -> object:
        raise RuntimeError("no DB in unit test")


@pytest.fixture(autouse=True)
def _override_db() -> Iterator[None]:
    """get_db → заглушка без БД на время API-тестов концепции (conftest чистит overrides)."""

    def _fake_get_db() -> Iterator[_NoDbSession]:
        yield _NoDbSession()

    app.dependency_overrides[get_db] = _fake_get_db
    yield
    app.dependency_overrides.pop(get_db, None)


_PARCEL = {
    "type": "Polygon",
    "coordinates": [
        [
            [60.60, 56.830],
            [60.6045, 56.830],
            [60.6045, 56.8328],
            [60.60, 56.8328],
            [60.60, 56.830],
        ]
    ],
}


def _post(payload: dict[str, object]) -> object:
    client = TestClient(app)
    return client.post("/api/v1/concepts", json=payload)


def test_concepts_returns_three_filled_variants() -> None:
    response = _post(
        {
            "parcel_geojson": _PARCEL,
            "housing_class": "comfort",
            "target_floors": 9,
            "development_type": "mid_rise",
            "land_cost_rub": 150_000_000,
        }
    )
    assert response.status_code == 200, response.text
    variants = response.json()["variants"]
    assert len(variants) == 3
    assert {v["strategy"] for v in variants} == {"max_area", "max_insolation", "balanced"}

    for v in variants:
        teap = v["teap"]
        fin = v["financial"]
        # Реальные, ненулевые числа (не stub-нули).
        assert teap["built_area_sqm"] > 0
        assert teap["total_floor_area_sqm"] > 0
        assert teap["residential_area_sqm"] > 0
        assert teap["apartments_count"] > 0
        assert teap["density"] > 0
        assert teap["parking_spaces"] > 0
        assert fin["revenue_rub"] > 0
        assert fin["cost_rub"] > 0
        # PR-2: без БД market-price lookup честно падает в норматив класса.
        assert fin["price_is_calibrated"] is False
        assert fin["price_source"] == "class_norm"
        assert fin["price_per_sqm_used"] > 0
        # GeoJSON застройки непустой.
        fc = v["buildings_geojson"]
        assert fc["type"] == "FeatureCollection"
        assert len(fc["features"]) > 0


def test_concepts_degenerate_parcel_returns_422() -> None:
    tiny = {
        "type": "Polygon",
        "coordinates": [
            [
                [60.60, 56.830],
                [60.60015, 56.830],
                [60.60015, 56.83015],
                [60.60, 56.83015],
                [60.60, 56.830],
            ]
        ],
    }
    response = _post(
        {
            "parcel_geojson": tiny,
            "housing_class": "comfort",
            "target_floors": 9,
            "development_type": "mid_rise",
        }
    )
    assert response.status_code == 422


def test_concepts_projected_coordinates_returns_422_not_500() -> None:
    # Регресс эпик #2445 B1: parcel_geojson в проекции (МСК-66/UTM-like метры) вместо
    # WGS84 lon/lat раньше падал неперехваченным pyproj.CRSError -> opaque 500. Должен
    # быть чистый 422 (ParcelGeometryError), как и для прочих невалидных геометрий.
    projected = {
        "type": "Polygon",
        "coordinates": [
            [
                [500_000.0, 6_200_000.0],
                [500_450.0, 6_200_000.0],
                [500_450.0, 6_200_310.0],
                [500_000.0, 6_200_310.0],
                [500_000.0, 6_200_000.0],
            ]
        ],
    }
    response = _post(
        {
            "parcel_geojson": projected,
            "housing_class": "comfort",
            "target_floors": 9,
            "development_type": "mid_rise",
        }
    )
    assert response.status_code == 422, response.text


def test_concepts_response_matches_contract_keys() -> None:
    response = _post(
        {
            "parcel_geojson": _PARCEL,
            "housing_class": "business",
            "target_floors": 16,
            "development_type": "high_rise",
        }
    )
    assert response.status_code == 200
    variant = response.json()["variants"][0]
    assert set(variant.keys()) == {
        "strategy",
        "buildings_geojson",
        "teap",
        "financial",
        # Stage 3a (#1965): partial-fit signal (None в greedy-режиме, число в program-режиме).
        "placed_count",
        "requested_count",
    }
    # Greedy-режим (программа не задана) → сигнал частичного размещения пуст (backward-compat).
    assert variant["placed_count"] is None
    assert variant["requested_count"] is None
    assert set(variant["teap"].keys()) == {
        "built_area_sqm",
        "total_floor_area_sqm",
        "office_area_sqm",
        "residential_area_sqm",
        "apartments_count",
        "density",
        "parking_spaces",
    }
    # Stage 3a partial-fit signal присутствует в контракте (None в greedy-режиме).
    assert variant["placed_count"] is None
    assert variant["requested_count"] is None
    assert set(variant["financial"].keys()) == {
        # legacy summary (backward-compat)
        "revenue_rub",
        "cost_rub",
        "gross_margin_rub",
        "irr",
        # revenue breakdown
        "revenue_residential_rub",
        "revenue_parking_rub",
        "revenue_office_rub",
        # cost cascade
        "construction_rub",
        "pir_rub",
        "networks_rub",
        "developer_services_rub",
        "contingency_rub",
        "marketing_rub",
        "land_rub",
        # БДР / taxes
        "vat_rub",
        "profit_before_tax_rub",
        "profit_tax_rub",
        "net_profit_rub",
        # metrics
        "roi",
        "margin_pct",
        "irr_is_proxy",
        # DCF (PR-3)
        "npv_rub",
        "payback_months",
        "discount_rate_used",
        "schedule_is_default",
        "sales_duration_months",
        # financing overlay (PR-5)
        "financing_enabled",
        "annual_rate_used",
        "peak_debt_rub",
        "total_interest_rub",
        "net_profit_after_financing_rub",
        "financing_is_simplified",
        # levered (equity) IRR
        "levered_irr",
        "levered_irr_is_proxy",
        # price calibration (PR-2)
        "price_per_sqm_used",
        "price_is_calibrated",
        "price_source",
    }


# ── Stage 3a (#1965): program-driven placement через API ────────────────────────────


def test_concepts_program_places_single_variant_with_signal() -> None:
    # building_program задан → один вариант (не три), partial-fit сигнал заполнен.
    response = _post(
        {
            "parcel_geojson": _PARCEL,
            "housing_class": "comfort",
            "target_floors": 9,
            "development_type": "mid_rise",
            "building_program": [
                {"section_type": "monolith_comfort", "floors": 14, "count": 3},
                {"section_type": "panel_econom", "floors": 9, "count": 2},
            ],
        }
    )
    assert response.status_code == 200, response.text
    variants = response.json()["variants"]
    assert len(variants) == 1
    v = variants[0]
    assert v["requested_count"] == 5
    assert v["placed_count"] == 5
    assert v["teap"]["built_area_sqm"] > 0
    assert v["teap"]["apartments_count"] > 0
    # GeoJSON-фичи помечены program-режимом и несут тип секции.
    feats = v["buildings_geojson"]["features"]
    assert len(feats) == 5
    assert {f["properties"]["strategy"] for f in feats} == {"program"}


def test_concepts_program_unknown_type_returns_422() -> None:
    response = _post(
        {
            "parcel_geojson": _PARCEL,
            "housing_class": "comfort",
            "target_floors": 9,
            "development_type": "mid_rise",
            "building_program": [
                {"section_type": "does_not_exist", "floors": 10, "count": 1},
            ],
        }
    )
    assert response.status_code == 422
    assert "unknown house type" in response.json()["detail"].lower()


# ── Stage 3b (#1965): GET /api/v1/concepts/house-types catalog ──────────────────────


def test_house_types_catalog_returns_full_catalog() -> None:
    # Каталог отдаётся read-only (GET, без БД) и зеркалит catalog.HOUSE_TYPES —
    # single source of truth для пикера Stage 3b (фронт ничего не хардкодит).
    from app.services.generative.catalog import HOUSE_TYPES

    client = TestClient(app)
    resp = client.get("/api/v1/concepts/house-types")
    assert resp.status_code == 200, resp.text
    items = resp.json()["house_types"]
    # Полный каталог: один пункт на каждый HouseType, ключи совпадают с каталогом.
    assert len(items) == len(HOUSE_TYPES)
    assert {it["section_type"] for it in items} == {ht.section_type for ht in HOUSE_TYPES}

    # Каждый пункт несёт всё, что нужно пикеру: лейбл, габариты, дефолт-этажность, класс.
    by_key = {it["section_type"]: it for it in items}
    for ht in HOUSE_TYPES:
        item = by_key[ht.section_type]
        assert item["label_ru"] == ht.label_ru
        assert item["footprint_w_m"] == ht.footprint_w_m
        assert item["footprint_d_m"] == ht.footprint_d_m
        # footprint_sqm — производный helper для UI (ширина × глубина).
        assert item["footprint_sqm"] == ht.footprint_w_m * ht.footprint_d_m
        assert item["default_floors"] == ht.default_floors
        assert item["housing_class"] == ht.housing_class
        # default_floors в пределах контракта BuildingProgramItem [1, 40].
        assert 1 <= item["default_floors"] <= 40


def test_house_types_catalog_keys_match_available_section_types() -> None:
    # Ключи каталога == множество, валидируемое в create_concept (program-режим).
    # Гарантирует: всё, что фронт может выбрать, бэк примет (нет рассинхрона 422).
    from app.services.generative.catalog import available_section_types

    client = TestClient(app)
    resp = client.get("/api/v1/concepts/house-types")
    assert resp.status_code == 200, resp.text
    catalog_keys = {it["section_type"] for it in resp.json()["house_types"]}
    assert catalog_keys == set(available_section_types())
