from __future__ import annotations from uuid import uuid4 from geoalchemy2.shape import from_shape, to_shape from pyproj import Transformer import pytest from shapely.geometry import Polygon, mapping from shapely.ops import transform from app.core.errors import AppError from app.models import Area, Project from app.schemas.area import AreaCreate, AreaUpdate from app.services.area_service import AreaService from app.utils.geometry import area_m2, normalize_area_to_epsg4326 class FakeSession: def __init__(self, objects=None) -> None: self.objects = objects or {} self.added = [] self.commits = 0 self.refreshes = [] def get(self, model, item_id): return self.objects.get((model, item_id)) def add(self, item) -> None: self.added.append(item) def commit(self) -> None: self.commits += 1 def refresh(self, item) -> None: self.refreshes.append(item) def _wgs84_polygon(offset: float = 0.0) -> Polygon: return Polygon( [ (5.00 + offset, 51.00), (5.01 + offset, 51.00), (5.01 + offset, 51.01), (5.00 + offset, 51.01), (5.00 + offset, 51.00), ] ) def _to_lambert(geometry: Polygon) -> Polygon: transformer = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True) return transform(transformer.transform, geometry) def test_create_area_transforms_declared_lambert_geometry_before_storage() -> None: project_id = uuid4() db = FakeSession({(Project, project_id): Project(id=project_id, name="Belgium")}) source = _wgs84_polygon() area = AreaService.create_area( db, project_id, AreaCreate(name="Lambert AOI", geometry=mapping(_to_lambert(source)), crs="EPSG:31370"), ) stored = to_shape(area.geometry) assert stored.bounds == pytest.approx(source.bounds, abs=1e-7) assert area.original_crs == "EPSG:31370" assert area.area_m2 == pytest.approx(area_m2(normalize_area_to_epsg4326(mapping(source), "EPSG:4326")[0])) assert area.area_m2 and area.area_m2 > 0 assert to_shape(area.bbox).bounds == pytest.approx(source.bounds, abs=1e-7) def test_patch_area_replaces_geometry_and_recomputes_all_spatial_fields() -> None: area_id = uuid4() project_id = uuid4() original = _wgs84_polygon() normalized, _ = normalize_area_to_epsg4326(mapping(original), "EPSG:4326") area = Area( id=area_id, project_id=project_id, name="Original", geometry=from_shape(normalized, srid=4326), bbox=from_shape(normalized.envelope, srid=4326), original_crs="EPSG:4326", area_m2=area_m2(normalized), ) db = FakeSession({(Area, area_id): area}) replacement = _wgs84_polygon(offset=0.05) updated = AreaService.update_area( db, area_id, AreaUpdate( name="Replacement", geometry=mapping(_to_lambert(replacement)), crs="EPSG:31370", ), ) assert updated.name == "Replacement" assert updated.original_crs == "EPSG:31370" assert to_shape(updated.geometry).bounds == pytest.approx(replacement.bounds, abs=1e-7) assert to_shape(updated.bbox).bounds == pytest.approx(replacement.bounds, abs=1e-7) assert updated.area_m2 and updated.area_m2 > 0 assert db.commits == 1 @pytest.mark.parametrize( ("geometry", "crs", "message_fragment"), [ (mapping(_wgs84_polygon()), "EPSG:not-real", "unknown or invalid"), (mapping(_wgs84_polygon()), "EPSG:4979", "exactly two spatial axes"), ( { "type": "Polygon", "coordinates": [[[5.0, 51.0], [float("nan"), 51.0], [5.1, 51.1], [5.0, 51.0]]], }, "EPSG:4326", "finite", ), (mapping(Polygon([(10.0, 51.0), (10.1, 51.0), (10.1, 51.1), (10.0, 51.0)])), "EPSG:4326", "workbench domain"), ( { "type": "Polygon", "coordinates": [[[5.0, 51.0], [5.1, 51.1], [5.1, 51.0], [5.0, 51.1], [5.0, 51.0]]], }, "EPSG:4326", "invalid", ), ({"type": "Point", "coordinates": [5.0, 51.0]}, "EPSG:4326", "Polygon or MultiPolygon"), ], ) def test_create_area_rejects_invalid_crs_nonfinite_and_out_of_domain_geometry( geometry: dict, crs: str, message_fragment: str, ) -> None: project_id = uuid4() db = FakeSession({(Project, project_id): Project(id=project_id, name="Belgium")}) with pytest.raises(AppError) as exc_info: AreaService.create_area(db, project_id, AreaCreate(name="Invalid", geometry=geometry, crs=crs)) assert exc_info.value.code == "INVALID_GEOMETRY" assert message_fragment in exc_info.value.message assert db.commits == 0 def test_patch_area_rejects_crs_without_replacement_geometry() -> None: area_id = uuid4() area = Area(id=area_id, project_id=uuid4(), name="AOI", original_crs="EPSG:4326") db = FakeSession({(Area, area_id): area}) with pytest.raises(AppError) as exc_info: AreaService.update_area(db, area_id, AreaUpdate(crs="EPSG:31370")) assert exc_info.value.code == "INVALID_AREA_CRS_UPDATE" assert db.commits == 0