"""Stage 1b tests — greedy placement, STRtree collisions, gaps, strategies.

Asserts the structural guarantees of the greedy sweep: footprints stay inside the
buildable area, respect the inter-section gap (no overlaps), the three strategies
differ, the coverage cap bounds density, and the result is deterministic.
"""

from __future__ import annotations

from shapely.geometry import shape

from app.schemas.concept import BuildingProgramItem, ConceptInput
from app.services.generative import catalog, geometry, placement

_PARCEL_COORDS = [
    [60.60, 56.830],
    [60.6045, 56.830],
    [60.6045, 56.8328],
    [60.60, 56.8328],
    [60.60, 56.830],
]


def _payload(**overrides: object) -> ConceptInput:
    base: dict[str, object] = {
        "parcel_geojson": {"type": "Polygon", "coordinates": [_PARCEL_COORDS]},
        "housing_class": "comfort",
        "target_floors": 9,
        "development_type": "mid_rise",
    }
    base.update(overrides)
    return ConceptInput(**base)  # type: ignore[arg-type]


def test_all_three_strategies_present() -> None:
    parcel = geometry.parse_parcel(_payload())
    variants = placement.place_all_strategies(parcel, _payload())
    assert {v.strategy for v in variants} == {"max_area", "max_insolation", "balanced"}


def test_footprints_inside_buildable_and_non_overlapping() -> None:
    parcel = geometry.parse_parcel(_payload())
    spec = next(s for s in placement._STRATEGIES if s.name == "max_area")
    footprints = placement._greedy_place(parcel, spec, coverage_cap=0.45)
    assert len(footprints) > 0
    for fp in footprints:
        # Внутри пятна застройки (с допуском на численную погрешность буфера).
        assert parcel.buildable_m.buffer(0.01).covers(fp)
    # Никакие две секции не перекрываются (разрыв gap_m выдержан).
    for i, a in enumerate(footprints):
        for b in footprints[i + 1 :]:
            assert not a.buffer(-0.01).intersects(b.buffer(-0.01))


def test_gap_between_sections_respected() -> None:
    parcel = geometry.parse_parcel(_payload())
    spec = next(s for s in placement._STRATEGIES if s.name == "max_insolation")
    footprints = placement._greedy_place(parcel, spec, coverage_cap=0.45)
    # Минимальное расстояние между любыми двумя секциями >= gap_m (с допуском).
    for i, a in enumerate(footprints):
        for b in footprints[i + 1 :]:
            assert a.distance(b) >= spec.gap_m - 0.5


def test_max_area_denser_than_max_insolation() -> None:
    payload = _payload()
    parcel = geometry.parse_parcel(payload)
    variants = {v.strategy: v for v in placement.place_all_strategies(parcel, payload)}
    # Максимум площади даёт большее пятно/жилую, чем максимум инсоляции.
    assert variants["max_area"].teap.built_area_sqm > variants["max_insolation"].teap.built_area_sqm
    assert (
        variants["max_area"].teap.residential_area_sqm
        > variants["max_insolation"].teap.residential_area_sqm
    )


def test_coverage_cap_bounds_built_area() -> None:
    # high_rise cap = 0.50; пятно не должно его превышать (+небольшой запас на 1 секцию).
    payload = _payload(development_type="high_rise")
    parcel = geometry.parse_parcel(payload)
    variants = placement.place_all_strategies(parcel, payload)
    cap = placement._COVERAGE_CAP_BY_TYPE["high_rise"]
    for v in variants:
        coverage = v.teap.built_area_sqm / parcel.buildable_area_sqm
        # +0.05: цикл останавливается ПОСЛЕ секции, перешагнувшей порог.
        assert coverage <= cap + 0.05


def test_buildings_geojson_features_are_valid_polygons() -> None:
    payload = _payload()
    parcel = geometry.parse_parcel(payload)
    variant = placement.place_all_strategies(parcel, payload)[0]
    fc = variant.buildings_geojson
    assert fc["type"] == "FeatureCollection"
    features = fc["features"]
    assert isinstance(features, list) and len(features) > 0
    for feat in features:
        geom = shape(feat["geometry"])
        assert geom.geom_type == "Polygon"
        assert geom.is_valid
        assert feat["properties"]["floors"] >= 1


def test_program_footprint_override_changes_section_area() -> None:
    # Stage 3c: ручное пятно (footprint_w_m/_d_m) кладётся вместо каталожного.
    parcel = geometry.parse_parcel(_payload())
    section_type = next(iter(catalog.available_section_types()))
    house = catalog.get_house_type(section_type)

    override = BuildingProgramItem(
        section_type=section_type,
        floors=10,
        count=2,
        footprint_w_m=10.0,
        footprint_d_m=10.0,
    )
    placed = placement.place_program(parcel, [override])
    assert placed.placed_count >= 1
    # Каждая размещённая секция несёт пятно override (10×10 = 100 м²), не каталог.
    for sec in placed.sections:
        assert abs(sec.footprint.area - 100.0) < 1.0

    # Без override — пятно из каталога (контроль: байт-в-байт прежнее поведение).
    base = BuildingProgramItem(section_type=section_type, floors=10, count=2)
    placed_base = placement.place_program(parcel, [base])
    catalog_area = house.footprint_w_m * house.footprint_d_m
    for sec in placed_base.sections:
        assert abs(sec.footprint.area - catalog_area) < 1.0


def test_program_partial_footprint_falls_back_to_catalog() -> None:
    # Только ширина задана (без глубины) → трактуем как «не задано», берём каталог.
    parcel = geometry.parse_parcel(_payload())
    section_type = next(iter(catalog.available_section_types()))
    house = catalog.get_house_type(section_type)
    item = BuildingProgramItem(section_type=section_type, floors=10, count=1, footprint_w_m=10.0)
    placed = placement.place_program(parcel, [item])
    catalog_area = house.footprint_w_m * house.footprint_d_m
    for sec in placed.sections:
        assert abs(sec.footprint.area - catalog_area) < 1.0


def test_placement_deterministic() -> None:
    payload = _payload()
    p1 = geometry.parse_parcel(payload)
    p2 = geometry.parse_parcel(payload)
    v1 = placement.place_all_strategies(p1, payload)
    v2 = placement.place_all_strategies(p2, payload)
    for a, b in zip(v1, v2, strict=True):
        assert a.teap.apartments_count == b.teap.apartments_count
        assert a.teap.built_area_sqm == b.teap.built_area_sqm
        assert len(a.buildings_geojson["features"]) == len(b.buildings_geojson["features"])
