from __future__ import annotations from types import SimpleNamespace from unittest.mock import patch from django.test import TestCase, override_settings from rest_framework.test import APIClient from plant.models import Plant from .growth_simulation import paginate_growth_stages, run_growth_simulation @override_settings(ROOT_URLCONF="crop_simulation.urls") class PlantGrowthSimulationApiTests(TestCase): def setUp(self): self.client = APIClient() self.plant = Plant.objects.create( name="گوجه‌فرنگی", growth_profile={ "base_temperature": 10, "required_gdd_for_maturity": 1200, "current_cumulative_gdd": 50, }, ) self.weather = [ { "DAY": "2026-04-01", "LAT": 35.7, "LON": 51.4, "TMIN": 12, "TMAX": 24, "RAIN": 0.0, "ET0": 0.32, }, { "DAY": "2026-04-02", "LAT": 35.7, "LON": 51.4, "TMIN": 13, "TMAX": 25, "RAIN": 0.0, "ET0": 0.34, }, { "DAY": "2026-04-03", "LAT": 35.7, "LON": 51.4, "TMIN": 14, "TMAX": 27, "RAIN": 1.0, "ET0": 0.36, }, ] def test_run_growth_simulation_returns_stage_timeline(self): with patch("crop_simulation.growth_simulation._run_simulation") as mock_run_simulation: mock_run_simulation.return_value = ( { "engine": "pcse", "model_name": "wofost", "metrics": {"yield_estimate": 10.0}, "daily_output": [ {"DAY": "2026-04-01", "DVS": 0.1, "LAI": 0.2, "TAGP": 10.0}, {"DAY": "2026-04-02", "DVS": 0.3, "LAI": 0.4, "TAGP": 20.0}, {"DAY": "2026-04-03", "DVS": 1.1, "LAI": 0.6, "TAGP": 30.0}, ], }, 12, None, ) result = run_growth_simulation( { "plant_name": self.plant.name, "dynamic_parameters": ["DVS", "LAI", "TAGP"], "weather": self.weather, "soil_parameters": {"SMFCF": 0.34, "SMW": 0.14, "RDMSOL": 120.0}, "site_parameters": {"WAV": 40.0}, "page_size": 2, } ) self.assertEqual(result["plant_name"], self.plant.name) self.assertGreaterEqual(result["daily_records_count"], 3) self.assertTrue(result["stage_timeline"]) self.assertEqual(result["pagination"]["page_size"], 2) @patch("crop_simulation.views.run_growth_simulation_task.delay") def test_queue_api_returns_task_id(self, mock_delay): mock_delay.return_value = SimpleNamespace(id="growth-task-1") response = self.client.post( "/growth/", data={ "plant_name": self.plant.name, "dynamic_parameters": ["DVS", "LAI"], "weather": self.weather, }, format="json", ) self.assertEqual(response.status_code, 202) self.assertEqual(response.json()["data"]["task_id"], "growth-task-1") @patch("crop_simulation.views._get_async_result") def test_status_api_returns_paginated_stages(self, mock_get_async_result): stage_timeline = [ { "order": 1, "stage_code": "establishment", "stage_name": "استقرار", "start_date": "2026-04-01", "end_date": "2026-04-02", "days_count": 2, "metrics": {"DVS": {"start": 0.1, "end": 0.2, "min": 0.1, "max": 0.2, "avg": 0.15}}, }, { "order": 2, "stage_code": "vegetative", "stage_name": "رشد رویشی", "start_date": "2026-04-03", "end_date": "2026-04-05", "days_count": 3, "metrics": {"DVS": {"start": 0.3, "end": 0.8, "min": 0.3, "max": 0.8, "avg": 0.55}}, }, { "order": 3, "stage_code": "flowering", "stage_name": "گلدهی", "start_date": "2026-04-06", "end_date": "2026-04-07", "days_count": 2, "metrics": {"DVS": {"start": 1.0, "end": 1.2, "min": 1.0, "max": 1.2, "avg": 1.1}}, }, ] mock_get_async_result.return_value = SimpleNamespace( state="SUCCESS", result={ "plant_name": self.plant.name, "dynamic_parameters": ["DVS"], "engine": "growth_projection", "model_name": "growth_projection_v1", "scenario_id": None, "simulation_warning": None, "summary_metrics": {}, "stage_timeline": stage_timeline, "stages_page": stage_timeline[:1], "pagination": paginate_growth_stages(stage_timeline, page=1, page_size=1)["pagination"], "daily_records_count": 7, "default_page_size": 1, }, ) response = self.client.get("/growth/growth-task-1/status/?page=2&page_size=1") self.assertEqual(response.status_code, 200) payload = response.json()["data"]["result"] self.assertEqual(payload["pagination"]["page"], 2) self.assertEqual(len(payload["stages_page"]), 1) self.assertEqual(payload["stages_page"][0]["stage_code"], "vegetative") @patch("crop_simulation.views.apps.get_app_config") def test_current_farm_chart_api_returns_simulation_payload(self, mock_get_app_config): mock_simulator = SimpleNamespace( simulate=lambda **_kwargs: { "farm_uuid": "550e8400-e29b-41d4-a716-446655440000", "plant_name": self.plant.name, "engine": "growth_projection", "model_name": "growth_projection_v1", "scenario_id": 12, "simulation_warning": None, "categories": ["2026-04-01", "2026-04-02"], "series": [ {"name": "تعداد برگ تخمینی", "key": "leaf_count_estimate", "data": [120.0, 140.0]}, {"name": "وزن بیوماس", "key": "biomass_weight", "data": [35.0, 45.0]}, ], "summary": [ { "title": "تعداد برگ تخمینی", "subtitle": "وضعیت فعلی", "amount": 140.0, "unit": "leaf", "avatarColor": "success", "avatarIcon": "tabler-leaf", } ], "current_state": { "date": "2026-04-02", "leaf_count_estimate": 140.0, "leaf_area_index": 0.0117, "biomass_weight": 45.0, "storage_organ_weight": 10.0, "soil_moisture_percent": 41.2, "development_stage": 0.35, "gdd": 9.0, }, "metrics": {"yield_estimate": 10.0}, "daily_output": [], } ) mock_get_app_config.return_value = SimpleNamespace( get_current_farm_chart_simulator=lambda: mock_simulator ) response = self.client.post( "/current-farm-chart/", data={ "farm_uuid": "550e8400-e29b-41d4-a716-446655440000", "plant_name": self.plant.name, }, format="json", ) self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["plant_name"], self.plant.name) self.assertEqual(payload["scenario_id"], 12) self.assertEqual(payload["current_state"]["leaf_count_estimate"], 140.0) self.assertEqual(payload["series"][0]["key"], "leaf_count_estimate") @patch("crop_simulation.views.apps.get_app_config") def test_harvest_prediction_api_returns_payload(self, mock_get_app_config): mock_service = SimpleNamespace( get_harvest_prediction=lambda **_kwargs: { "date": "2026-05-14", "dateFormatted": "14 May 2026", "daysUntil": 43, "description": "شبيه ساز نشان مي دهد حدود 43 روز ديگر تا برداشت باقي مانده است.", "optimalWindowStart": "2026-05-11", "optimalWindowEnd": "2026-05-17", "gddDetails": { "current_cumulative_gdd": 50.0, "required_gdd_for_maturity": 1200.0, "remaining_gdd": 1150.0, "simulation_engine": "growth_projection", }, } ) mock_get_app_config.return_value = SimpleNamespace( get_harvest_prediction_service=lambda: mock_service ) response = self.client.post( "/harvest-prediction/", data={ "farm_uuid": "550e8400-e29b-41d4-a716-446655440000", "plant_name": self.plant.name, }, format="json", ) self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["daysUntil"], 43) self.assertEqual(payload["gddDetails"]["simulation_engine"], "growth_projection") @patch("crop_simulation.views.apps.get_app_config") def test_yield_prediction_api_returns_payload(self, mock_get_app_config): mock_service = SimpleNamespace( get_yield_prediction=lambda **_kwargs: { "farm_uuid": "550e8400-e29b-41d4-a716-446655440000", "plant_name": self.plant.name, "predictedYieldTons": 5.4, "predictedYieldRaw": 5400.0, "unit": "تن", "sourceUnit": "kg/ha", "simulationEngine": "growth_projection", "simulationModel": "growth_projection_v1", "scenarioId": 12, "simulationWarning": None, "supportingMetrics": {"yield_estimate": 5400.0}, } ) mock_get_app_config.return_value = SimpleNamespace( get_yield_prediction_service=lambda: mock_service ) response = self.client.post( "/yield-prediction/", data={ "farm_uuid": "550e8400-e29b-41d4-a716-446655440000", "plant_name": self.plant.name, }, format="json", ) self.assertEqual(response.status_code, 200) payload = response.json()["data"] self.assertEqual(payload["predictedYieldTons"], 5.4) self.assertEqual(payload["sourceUnit"], "kg/ha")