from datetime import date from types import SimpleNamespace from unittest.mock import patch from django.test import TestCase, override_settings from rest_framework.test import APIClient from location_data.models import ( AnalysisGridCell, AnalysisGridObservation, BlockSubdivision, RemoteSensingClusterAssignment, RemoteSensingRun, RemoteSensingSubdivisionResult, SoilLocation, ) @override_settings(ROOT_URLCONF="location_data.urls") class RemoteSensingApiTests(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.location.set_input_block_count(1) self.location.save(update_fields=["input_block_count", "block_layout", "updated_at"]) self.subdivision = BlockSubdivision.objects.create( soil_location=self.location, block_code="block-1", source_boundary=self.boundary, chunk_size_sqm=900, status="created", ) def test_post_remote_sensing_returns_404_when_location_missing(self): response = self.client.post( "/remote-sensing/", data={ "lat": 35.7000, "lon": 51.4000, "start_date": "2025-01-01", "end_date": "2025-01-31", }, format="json", ) self.assertEqual(response.status_code, 404) self.assertEqual(response.json()["msg"], "location پیدا نشد.") @patch("location_data.views.run_remote_sensing_analysis_task.delay") def test_post_remote_sensing_enqueues_task_and_returns_processing(self, mock_delay): mock_delay.return_value = SimpleNamespace(id="task-123") response = self.client.post( "/remote-sensing/", data={ "lat": 35.6892, "lon": 51.3890, "block_code": "block-1", "start_date": "2025-01-01", "end_date": "2025-01-31", "force_refresh": False, }, format="json", ) self.assertEqual(response.status_code, 202) payload = response.json()["data"] self.assertEqual(payload["status"], "processing") self.assertEqual(payload["source"], "processing") self.assertEqual(payload["task_id"], "task-123") self.assertEqual(payload["block_code"], "block-1") self.assertEqual(payload["summary"]["cell_count"], 0) run = RemoteSensingRun.objects.get(id=payload["run"]["id"]) self.assertEqual(run.block_code, "block-1") self.assertEqual(run.status, RemoteSensingRun.STATUS_PENDING) self.assertEqual(run.metadata["stage"], "queued") self.assertEqual(run.metadata["selected_features"], []) mock_delay.assert_called_once() def test_get_remote_sensing_returns_processing_when_run_exists_without_results(self): 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_RUNNING, metadata={"task_id": "task-123"}, ) response = self.client.get( "/remote-sensing/", data={ "lat": 35.6892, "lon": 51.3890, "block_code": "block-1", "start_date": "2025-01-01", "end_date": "2025-01-31", }, ) self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["status"], "processing") self.assertEqual(payload["source"], "processing") self.assertEqual(payload["cells"], []) self.assertEqual(payload["run"]["status"], RemoteSensingRun.STATUS_RUNNING) def test_get_remote_sensing_returns_cached_results(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, ) cell = AnalysisGridCell.objects.create( soil_location=self.location, block_subdivision=self.subdivision, block_code="block-1", cell_code="cell-1", chunk_size_sqm=900, geometry=self.boundary, centroid_lat="35.689500", centroid_lon="51.389500", ) AnalysisGridObservation.objects.create( cell=cell, run=run, temporal_start=date(2025, 1, 1), temporal_end=date(2025, 1, 31), ndvi=0.61, ndwi=0.22, lst_c=24.5, soil_vv=0.13, soil_vv_db=-8.860566, dem_m=1550.0, slope_deg=4.2, metadata={"backend_name": "openeo"}, ) response = self.client.get( "/remote-sensing/", data={ "lat": 35.6892, "lon": 51.3890, "block_code": "block-1", "start_date": "2025-01-01", "end_date": "2025-01-31", }, ) self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["status"], "success") self.assertEqual(payload["source"], "database") self.assertEqual(payload["summary"]["cell_count"], 1) self.assertEqual(payload["summary"]["ndvi_mean"], 0.61) self.assertEqual(payload["summary"]["soil_vv_db_mean"], -8.860566) self.assertEqual(len(payload["cells"]), 1) self.assertEqual(payload["cells"][0]["cell_code"], "cell-1") def test_run_status_endpoint_returns_normalized_status(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", "selected_features": ["ndvi"]}, ) response = self.client.get(f"/remote-sensing/runs/{run.id}/status/") self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["status"], "completed") self.assertEqual(payload["run"]["pipeline_status"], "completed") self.assertEqual(payload["run"]["stage"], "completed") self.assertEqual(payload["run"]["selected_features"], ["ndvi"]) def test_run_result_endpoint_returns_paginated_assignments(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"}, ) cell = AnalysisGridCell.objects.create( soil_location=self.location, block_subdivision=self.subdivision, block_code="block-1", cell_code="cell-1", chunk_size_sqm=900, geometry=self.boundary, centroid_lat="35.689500", centroid_lon="51.389500", ) AnalysisGridObservation.objects.create( cell=cell, run=run, temporal_start=date(2025, 1, 1), temporal_end=date(2025, 1, 31), ndvi=0.61, ndwi=0.22, lst_c=24.5, soil_vv=0.13, soil_vv_db=-8.860566, dem_m=1550.0, slope_deg=4.2, metadata={"backend_name": "openeo"}, ) result = RemoteSensingSubdivisionResult.objects.create( soil_location=self.location, run=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"], metadata={"used_cell_count": 1, "skipped_cell_count": 0}, ) RemoteSensingClusterAssignment.objects.create( result=result, cell=cell, cluster_label=0, raw_feature_values={"ndvi": 0.61}, scaled_feature_values={"ndvi": 0.0}, ) response = self.client.get(f"/remote-sensing/runs/{run.id}/result/", data={"page": 1, "page_size": 10}) self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["status"], "completed") self.assertEqual(payload["subdivision_result"]["cluster_count"], 1) self.assertEqual(len(payload["subdivision_result"]["assignments"]), 1) self.assertEqual(payload["pagination"]["assignments"]["total_items"], 1)