from datetime import date, timedelta import uuid from unittest.mock import patch from django.test import TestCase, override_settings from django.utils import timezone from rest_framework.test import APIClient from location_data.models import ( AnalysisGridCell, BlockSubdivision, RemoteSensingClusterBlock, RemoteSensingRun, RemoteSensingSubdivisionResult, SoilLocation, ) @override_settings(ROOT_URLCONF="location_data.urls") class RemoteSensingClusterBlockLiveApiTests(TestCase): def setUp(self): self.client = APIClient() self.boundary = { "type": "Polygon", "coordinates": [ [ [51.3890, 35.6890], [51.3900, 35.6890], [51.3900, 35.6900], [51.3890, 35.6900], [51.3890, 35.6890], ] ], } self.location = SoilLocation.objects.create( latitude="35.689200", longitude="51.389000", farm_boundary=self.boundary, ) self.subdivision = BlockSubdivision.objects.create( soil_location=self.location, block_code="block-1", source_boundary=self.boundary, chunk_size_sqm=900, status="subdivided", ) self.run = RemoteSensingRun.objects.create( soil_location=self.location, block_subdivision=self.subdivision, block_code="block-1", chunk_size_sqm=900, temporal_start=date(2025, 1, 1), temporal_end=date(2025, 1, 31), status=RemoteSensingRun.STATUS_SUCCESS, metadata={"stage": "completed"}, ) self.result = RemoteSensingSubdivisionResult.objects.create( soil_location=self.location, run=self.run, block_subdivision=self.subdivision, block_code="block-1", chunk_size_sqm=900, temporal_start=date(2025, 1, 1), temporal_end=date(2025, 1, 31), cluster_count=1, selected_features=["ndvi", "ndwi", "soil_vv_db"], metadata={"used_cell_count": 2, "skipped_cell_count": 0}, ) self.cluster_block = RemoteSensingClusterBlock.objects.create( result=self.result, soil_location=self.location, block_subdivision=self.subdivision, block_code="block-1", sub_block_code="cluster-0", cluster_label=0, chunk_size_sqm=900, centroid_lat="35.689500", centroid_lon="51.389500", cell_count=2, cell_codes=["cell-1", "cell-2"], geometry=self.boundary, metadata={"source": "analysis_grid_cells"}, ) def test_get_cluster_block_live_returns_404_when_uuid_missing(self): response = self.client.get( f"/remote-sensing/cluster-blocks/{uuid.uuid4()}/live/" ) self.assertEqual(response.status_code, 404) self.assertEqual(response.json()["msg"], "زیر‌بلاک KMeans پیدا نشد.") @patch("location_data.views.compute_remote_sensing_metrics") def test_get_cluster_block_live_reads_fresh_metrics_from_openeo(self, compute_mock): compute_mock.return_value = { "results": { f"cluster-{self.cluster_block.uuid}": { "ndvi": 0.63, "ndwi": 0.21, "soil_vv": 0.13, "soil_vv_db": -8.860566, } }, "metadata": { "backend": "openeo", "backend_url": "https://openeofed.dataspace.copernicus.eu", "collections_used": ["SENTINEL2_L2A", "SENTINEL1_GRD"], "job_refs": {"ndvi": "job-1", "ndwi": "job-2", "soil_vv": "job-3"}, "failed_metrics": [], "payload_diagnostics": {}, }, } response = self.client.get( f"/remote-sensing/cluster-blocks/{self.cluster_block.uuid}/live/", data={"temporal_start": "2025-02-01", "temporal_end": "2025-02-15"}, ) self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["status"], "success") self.assertEqual(payload["source"], "openeo") self.assertEqual(payload["cluster_block"]["uuid"], str(self.cluster_block.uuid)) self.assertEqual(payload["summary"]["cell_count"], 2) self.assertEqual(payload["summary"]["ndvi_mean"], 0.63) self.assertEqual(payload["metrics"]["soil_vv_db"], -8.860566) self.assertEqual(payload["temporal_extent"]["start_date"], "2025-02-01") self.assertEqual(payload["temporal_extent"]["end_date"], "2025-02-15") self.assertEqual( payload["metadata"]["requested_cluster_uuid"], str(self.cluster_block.uuid), ) compute_mock.assert_called_once() args, kwargs = compute_mock.call_args self.assertEqual(len(args[0]), 1) self.assertEqual(args[0][0].geometry, self.boundary) self.assertEqual(kwargs["temporal_start"].isoformat(), "2025-02-01") self.assertEqual(kwargs["temporal_end"].isoformat(), "2025-02-15") @patch("location_data.views.compute_remote_sensing_metrics") def test_get_cluster_block_live_rebuilds_geometry_from_member_cells_when_missing(self, compute_mock): cell_geometry = { "type": "Polygon", "coordinates": [ [ [51.3890, 35.6890], [51.3895, 35.6890], [51.3895, 35.6895], [51.3890, 35.6895], [51.3890, 35.6890], ] ], } AnalysisGridCell.objects.create( soil_location=self.location, block_subdivision=self.subdivision, block_code="block-1", cell_code="cell-1", chunk_size_sqm=900, geometry=cell_geometry, centroid_lat="35.689250", centroid_lon="51.389250", ) self.cluster_block.geometry = {} self.cluster_block.save(update_fields=["geometry", "updated_at"]) compute_mock.return_value = { "results": { f"cluster-{self.cluster_block.uuid}": { "ndvi": 0.55, "ndwi": 0.18, "soil_vv": 0.10, "soil_vv_db": -10.0, } }, "metadata": {"job_refs": {}, "failed_metrics": [], "payload_diagnostics": {}}, } response = self.client.get( f"/remote-sensing/cluster-blocks/{self.cluster_block.uuid}/live/", data={"days": 7}, ) self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["metrics"]["ndvi"], 0.55) compute_mock.assert_called_once() args, kwargs = compute_mock.call_args self.assertEqual(args[0][0].geometry["type"], "Polygon") self.assertEqual(args[0][0].geometry, cell_geometry) expected_end = timezone.localdate() - timedelta(days=1) expected_start = expected_end - timedelta(days=6) self.assertEqual(kwargs["temporal_start"], expected_start) self.assertEqual(kwargs["temporal_end"], expected_end)