from datetime import date
from unittest.mock import patch
import uuid

from django.test import TestCase
from rest_framework.test import APIClient

from location_data.models import (
    AnalysisGridCell,
    AnalysisGridObservation,
    BlockSubdivision,
    RemoteSensingClusterAssignment,
    RemoteSensingClusterBlock,
    RemoteSensingRun,
    RemoteSensingSubdivisionResult,
    SoilLocation,
)
from farm_data.models import Device, PlantCatalogSnapshot, SensorData, SensorParameter
from farm_data.services import (
    assign_farm_plants_from_backend_ids,
    build_ai_farm_snapshot,
    get_canonical_farm_record,
    get_runtime_plant_for_farm,
    list_runtime_plants_for_farm,
)
from irrigation.models import IrrigationMethod
from weather.models import WeatherForecast

from farm_data.services import resolve_center_location_from_boundary


def square_boundary_for_center(lat: float, lon: float, delta: float = 0.01) -> dict:
    return {
        "type": "Polygon",
        "coordinates": [
            [
                [lon - delta, lat - delta],
                [lon + delta, lat - delta],
                [lon + delta, lat + delta],
                [lon - delta, lat + delta],
                [lon - delta, lat - delta],
            ]
        ],
    }


class FarmDetailApiTests(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.location = SoilLocation.objects.create(
            latitude="35.700000",
            longitude="51.400000",
            farm_boundary={"type": "Polygon", "coordinates": []},
        )
        self.weather = WeatherForecast.objects.create(
            location=self.location,
            forecast_date=date(2026, 4, 10),
            temperature_min=12.0,
            temperature_max=23.0,
            temperature_mean=18.0,
            precipitation=1.2,
            humidity_mean=52.0,
        )
        self.plant1 = PlantCatalogSnapshot.objects.create(backend_plant_id=101, name="گوجه‌فرنگی")
        self.plant2 = PlantCatalogSnapshot.objects.create(backend_plant_id=102, name="خیار")
        self.irrigation_method = IrrigationMethod.objects.create(name="آبیاری قطره‌ای")
        self.farm_uuid = uuid.uuid4()
        self.farm = SensorData.objects.create(
            farm_uuid=self.farm_uuid,
            center_location=self.location,
            weather_forecast=self.weather,
            irrigation_method=self.irrigation_method,
            sensor_payload={
                "sensor-7-1": {
                    "soil_moisture": 33.5,
                    "nitrogen": 99.0,
                }
            },
        )
        assign_farm_plants_from_backend_ids(self.farm, [self.plant2.backend_plant_id, self.plant1.backend_plant_id])

    def test_canonical_plant_runtime_path_uses_assignments_not_legacy_relation(self):
        farm = get_canonical_farm_record(str(self.farm_uuid))

        self.assertIsNotNone(farm)
        self.assertEqual([plant.name for plant in list_runtime_plants_for_farm(farm)], ["خیار", "گوجه‌فرنگی"])
        self.assertEqual(get_runtime_plant_for_farm(farm).name, "خیار")

    def test_assignment_sync_uses_backend_snapshots_as_canonical_source(self):
        self.assertEqual(
            list(self.farm.plant_assignments.values_list("plant__name", flat=True)),
            ["خیار", "گوجه‌فرنگی"],
        )

    def test_runtime_plant_lookup_resolves_by_name_from_canonical_assignments(self):
        farm = get_canonical_farm_record(str(self.farm_uuid))

        resolved = get_runtime_plant_for_farm(farm, plant_name="گوجه‌فرنگی")

        self.assertIsNotNone(resolved)
        self.assertEqual(resolved.name, "گوجه‌فرنگی")
        self.assertEqual(resolved.id, self.plant1.backend_plant_id)

    def test_returns_farm_detail_and_prioritizes_sensor_metrics_over_soil(self):
        response = self.client.get(f"/api/farm-data/{self.farm_uuid}/detail/")

        self.assertEqual(response.status_code, 200)
        payload = response.json()["data"]

        self.assertNotIn("farm_uuid", payload)
        self.assertEqual(payload["center_location"]["id"], self.location.id)
        self.assertEqual(payload["weather"]["id"], self.weather.id)
        self.assertEqual(
            payload["sensor_payload"]["sensor-7-1"]["soil_moisture"],
            33.5,
        )
        self.assertIn("sensor_schema", payload)
        self.assertEqual(payload["sensor_schema"]["sensor-7-1"][0]["code"], "nitrogen")

        resolved_metrics = payload["soil"]["resolved_metrics"]
        metric_sources = payload["soil"]["metric_sources"]

        self.assertEqual(resolved_metrics["nitrogen"], 99.0)
        self.assertEqual(metric_sources["nitrogen"]["type"], "sensor")
        self.assertEqual(metric_sources["nitrogen"]["strategy"], "single_value")
        self.assertEqual(payload["soil"]["satellite_snapshots"], [])
        self.assertCountEqual(payload["plant_ids"], [self.plant1.backend_plant_id, self.plant2.backend_plant_id])
        self.assertEqual(len(payload["plants"]), 2)
        returned_plants = {item["id"]: item for item in payload["plants"]}
        self.assertEqual(returned_plants[self.plant1.backend_plant_id]["name"], self.plant1.name)
        self.assertEqual(returned_plants[self.plant2.backend_plant_id]["name"], self.plant2.name)
        self.assertIn("light", returned_plants[self.plant1.backend_plant_id])
        self.assertEqual(len(payload["plant_assignments"]), 2)
        self.assertEqual(payload["irrigation_method_id"], self.irrigation_method.id)
        self.assertEqual(payload["irrigation_method"]["name"], self.irrigation_method.name)

    def test_returns_404_when_farm_is_missing(self):
        response = self.client.get(f"/api/farm-data/{uuid.uuid4()}/detail/")

        self.assertEqual(response.status_code, 404)
        self.assertEqual(response.json()["msg"], "farm یافت نشد.")

    def test_aggregates_conflicting_metrics_from_multiple_sensors_without_overwrite(self):
        self.farm.sensor_payload = {
            "sensor-a": {
                "soil_moisture": 20.0,
                "nitrogen": 90.0,
                "status": "ok",
            },
            "sensor-b": {
                "soil_moisture": 40.0,
                "nitrogen": 110.0,
                "status": "needs-check",
            },
        }
        self.farm.save(update_fields=["sensor_payload"])

        response = self.client.get(f"/api/farm-data/{self.farm_uuid}/detail/")

        self.assertEqual(response.status_code, 200)
        payload = response.json()["data"]
        resolved_metrics = payload["soil"]["resolved_metrics"]
        metric_sources = payload["soil"]["metric_sources"]

        self.assertEqual(resolved_metrics["soil_moisture"], 30.0)
        self.assertEqual(metric_sources["soil_moisture"]["strategy"], "average")
        self.assertCountEqual(
            metric_sources["soil_moisture"]["sensor_keys"],
            ["sensor-a", "sensor-b"],
        )
        self.assertEqual(metric_sources["soil_moisture"]["distinct_values"], [20.0, 40.0])
        self.assertEqual(resolved_metrics["status"], ["ok", "needs-check"])
        self.assertEqual(metric_sources["status"]["strategy"], "distinct_values")

    def test_detail_auto_registers_unknown_sensor_parameters(self):
        self.farm.sensor_payload = {
            "leaf-sensor": {
                "leaf_wetness": 11.0,
                "leaf_temperature": 19.8,
            }
        }
        self.farm.save(update_fields=["sensor_payload"])

        response = self.client.get(f"/api/farm-data/{self.farm_uuid}/detail/")

        self.assertEqual(response.status_code, 200)
        payload = response.json()["data"]
        leaf_schema = payload["sensor_schema"]["leaf-sensor"]
        self.assertCountEqual(
            [item["code"] for item in leaf_schema],
            ["leaf_temperature", "leaf_wetness"],
        )
        self.assertTrue(
            SensorParameter.objects.filter(sensor_key="leaf-sensor", code="leaf_wetness").exists()
        )

    def test_detail_aggregates_satellite_and_sensor_metrics_from_kmeans_sub_blocks_to_main_block(self):
        subdivision = BlockSubdivision.objects.create(
            soil_location=self.location,
            block_code="block-1",
            source_boundary=square_boundary_for_center(35.7, 51.4, delta=0.002),
            chunk_size_sqm=900,
            status="subdivided",
        )
        run = RemoteSensingRun.objects.create(
            soil_location=self.location,
            block_subdivision=subdivision,
            block_code="block-1",
            chunk_size_sqm=900,
            temporal_start=date(2026, 4, 1),
            temporal_end=date(2026, 4, 30),
            status=RemoteSensingRun.STATUS_SUCCESS,
        )
        result = RemoteSensingSubdivisionResult.objects.create(
            soil_location=self.location,
            run=run,
            block_subdivision=subdivision,
            block_code="block-1",
            chunk_size_sqm=900,
            temporal_start=run.temporal_start,
            temporal_end=run.temporal_end,
            cluster_count=2,
            selected_features=["ndvi", "ndwi", "soil_vv_db"],
            metadata={"used_cell_count": 3, "cluster_summaries": []},
        )
        cell_payloads = [
            ("cell-1", 0, 0.2, 10.0),
            ("cell-2", 0, 0.4, 12.0),
            ("cell-3", 1, 0.9, 20.0),
        ]
        created_cells = []
        for index, (cell_code, cluster_label, ndvi, ndwi) in enumerate(cell_payloads):
            cell = AnalysisGridCell.objects.create(
                soil_location=self.location,
                block_subdivision=subdivision,
                block_code="block-1",
                cell_code=cell_code,
                chunk_size_sqm=900,
                geometry=square_boundary_for_center(35.7 + (index * 0.0001), 51.4 + (index * 0.0001), delta=0.00005),
                centroid_lat=f"{35.7000 + (index * 0.0001):.6f}",
                centroid_lon=f"{51.4000 + (index * 0.0001):.6f}",
            )
            created_cells.append((cell, cluster_label))
            AnalysisGridObservation.objects.create(
                cell=cell,
                run=run,
                temporal_start=run.temporal_start,
                temporal_end=run.temporal_end,
                ndvi=ndvi,
                ndwi=ndwi,
                soil_vv_db=-8.0 - index,
            )
            RemoteSensingClusterAssignment.objects.create(
                result=result,
                cell=cell,
                cluster_label=cluster_label,
                raw_feature_values={},
                scaled_feature_values={},
            )

        cluster_0 = RemoteSensingClusterBlock.objects.create(
            result=result,
            soil_location=self.location,
            block_subdivision=subdivision,
            block_code="block-1",
            sub_block_code="cluster-0",
            cluster_label=0,
            chunk_size_sqm=900,
            centroid_lat="35.700050",
            centroid_lon="51.400050",
            cell_count=2,
            cell_codes=["cell-1", "cell-2"],
            geometry=square_boundary_for_center(35.70005, 51.40005, delta=0.00008),
            metadata={},
        )
        cluster_1 = RemoteSensingClusterBlock.objects.create(
            result=result,
            soil_location=self.location,
            block_subdivision=subdivision,
            block_code="block-1",
            sub_block_code="cluster-1",
            cluster_label=1,
            chunk_size_sqm=900,
            centroid_lat="35.700200",
            centroid_lon="51.400200",
            cell_count=1,
            cell_codes=["cell-3"],
            geometry=square_boundary_for_center(35.7002, 51.4002, delta=0.00008),
            metadata={},
        )
        self.location.block_layout = {
            "input_block_count": 1,
            "default_full_farm": True,
            "algorithm_status": "completed",
            "blocks": [
                {
                    "block_code": "block-1",
                    "order": 1,
                    "source": "input",
                    "boundary": square_boundary_for_center(35.7, 51.4, delta=0.002),
                    "needs_subdivision": True,
                    "sub_blocks": [
                        {
                            "cluster_uuid": str(cluster_0.uuid),
                            "sub_block_code": "cluster-0",
                            "cluster_label": 0,
                        },
                        {
                            "cluster_uuid": str(cluster_1.uuid),
                            "sub_block_code": "cluster-1",
                            "cluster_label": 1,
                        },
                    ],
                }
            ],
        }
        self.location.save(update_fields=["block_layout", "updated_at"])
        self.farm.sensor_payload = {
            "sensor-a": {
                "cluster_uuid": str(cluster_0.uuid),
                "soil_moisture": 10.0,
                "nitrogen": 100.0,
            },
            "sensor-b": {
                "cluster_uuid": str(cluster_0.uuid),
                "soil_moisture": 20.0,
                "nitrogen": 80.0,
            },
            "sensor-c": {
                "cluster_uuid": str(cluster_1.uuid),
                "soil_moisture": 30.0,
                "nitrogen": 60.0,
            },
        }
        self.farm.save(update_fields=["sensor_payload", "updated_at"])

        response = self.client.get(f"/api/farm-data/{self.farm_uuid}/detail/")

        self.assertEqual(response.status_code, 200)
        payload = response.json()["data"]
        block_snapshot = payload["soil"]["satellite_snapshots"][0]
        self.assertEqual(block_snapshot["block_code"], "block-1")
        self.assertEqual(block_snapshot["sub_block_count"], 2)
        self.assertEqual(block_snapshot["satellite_metrics"]["ndvi"], 0.6)
        self.assertEqual(block_snapshot["satellite_metrics"]["ndwi"], 15.5)
        self.assertEqual(block_snapshot["sensor_metrics"]["soil_moisture"], 22.5)
        self.assertEqual(block_snapshot["sensor_metrics"]["nitrogen"], 75.0)
        self.assertEqual(block_snapshot["resolved_metrics"]["soil_moisture"], 22.5)
        self.assertEqual(block_snapshot["metric_sources"]["ndvi"]["strategy"], "sub_block_mean_average")
        self.assertEqual(block_snapshot["metric_sources"]["soil_moisture"]["strategy"], "sub_block_mean_average")
        self.assertEqual(len(block_snapshot["satellite_sub_blocks"]), 2)
        self.assertEqual(len(block_snapshot["sensor_sub_blocks"]), 2)
        block_layout = payload["center_location"]["block_layout"]
        self.assertEqual(
            block_layout["blocks"][0]["aggregated_metrics"]["resolved_metrics"]["soil_moisture"],
            22.5,
        )
        self.assertEqual(
            block_layout["blocks"][0]["aggregated_metrics"]["satellite_metrics"]["ndvi"],
            0.6,
        )

    def test_detail_uses_farmer_aggregated_snapshot_as_canonical_soil_source(self):
        self.location.block_layout = {
            "blocks": [
                {"block_code": "block-a", "order": 1},
                {"block_code": "block-b", "order": 2},
            ]
        }
        self.location.save(update_fields=["block_layout", "updated_at"])

        with patch("farm_data.services.build_farmer_block_aggregated_snapshot") as aggregated_mock, patch(
            "farm_data.services.build_location_block_satellite_snapshots"
        ) as block_mock:
            aggregated_mock.return_value = {
                "status": "completed",
                "aggregation_strategy": "farmer_block_mean",
                "block_count": 2,
                "resolved_metrics": {"nitrogen": 42.0, "ndvi": 0.61},
                "metric_sources": {
                    "nitrogen": {"type": "farmer_block", "strategy": "average_of_main_blocks", "block_count": 2},
                    "ndvi": {"type": "farmer_block", "strategy": "average_of_main_blocks", "block_count": 2},
                },
            }
            block_mock.return_value = [
                {
                    "status": "completed",
                    "block_code": "block-a",
                    "resolved_metrics": {"nitrogen": 20.0, "ndvi": 0.5},
                    "metric_sources": {},
                    "satellite_sub_blocks": [{"sub_block_code": "cluster-a"}],
                    "sensor_sub_blocks": [],
                },
                {
                    "status": "completed",
                    "block_code": "block-b",
                    "resolved_metrics": {"nitrogen": 64.0, "ndvi": 0.72},
                    "metric_sources": {},
                    "satellite_sub_blocks": [{"sub_block_code": "cluster-b"}],
                    "sensor_sub_blocks": [],
                },
            ]

            response = self.client.get(f"/api/farm-data/{self.farm_uuid}/detail/")

        self.assertEqual(response.status_code, 200)
        payload = response.json()["data"]
        self.assertEqual(payload["soil"]["resolved_metrics"]["nitrogen"], 42.0)
        self.assertEqual(payload["soil"]["resolved_metrics"]["ndvi"], 0.61)
        self.assertEqual(payload["soil"]["source_metadata"]["canonical_source"], "farmer_block_aggregated_snapshot")
        self.assertEqual(payload["soil"]["source_metadata"]["policy"]["sensor"], "cluster_mean -> block_mean -> farm_mean")
        self.assertEqual(payload["source_metadata"]["weather"]["scope"], "location_center_based")
        self.assertEqual(len(payload["soil"]["block_snapshots"]), 2)
        self.assertEqual(len(payload["soil"]["cluster_breakdown"]), 2)
        aggregated_mock.assert_called_once()
        block_mock.assert_called_once()

    def test_detail_without_explicit_blocks_keeps_aggregated_snapshot_and_marks_compatibility_policy(self):
        with patch("farm_data.services.build_farmer_block_aggregated_snapshot") as aggregated_mock, patch(
            "farm_data.services.build_location_block_satellite_snapshots"
        ) as block_mock:
            aggregated_mock.return_value = {
                "status": "completed",
                "aggregation_strategy": "farmer_block_mean",
                "block_count": 1,
                "resolved_metrics": {"ndvi": 0.55},
                "metric_sources": {
                    "ndvi": {"type": "farmer_block", "strategy": "average_of_main_blocks", "block_count": 1},
                },
            }
            block_mock.return_value = [
                {
                    "status": "completed",
                    "block_code": "",
                    "resolved_metrics": {"ndvi": 0.55},
                    "metric_sources": {},
                    "satellite_sub_blocks": [],
                    "sensor_sub_blocks": [],
                }
            ]

            response = self.client.get(f"/api/farm-data/{self.farm_uuid}/detail/")

        self.assertEqual(response.status_code, 200)
        payload = response.json()["data"]
        self.assertEqual(payload["soil"]["resolved_metrics"]["ndvi"], 0.55)
        self.assertEqual(payload["soil"]["resolved_metrics"]["nitrogen"], 99.0)
        self.assertFalse(payload["soil"]["source_metadata"]["has_explicit_blocks"])
        self.assertTrue(payload["soil"]["source_metadata"]["compatibility_sensor_overlay_applied"])
        self.assertEqual(payload["soil"]["metric_sources"]["nitrogen"]["type"], "sensor")

    def test_detail_canonical_soil_metrics_do_not_come_from_single_raw_location_snapshot(self):
        with patch("farm_data.services.build_farmer_block_aggregated_snapshot") as aggregated_mock, patch(
            "farm_data.services.build_location_block_satellite_snapshots"
        ) as block_mock:
            aggregated_mock.return_value = {
                "status": "completed",
                "aggregation_strategy": "farmer_block_mean",
                "block_count": 1,
                "resolved_metrics": {"soil_moisture": 12.0},
                "metric_sources": {
                    "soil_moisture": {"type": "farmer_block", "strategy": "average_of_main_blocks", "block_count": 1},
                },
            }
            block_mock.return_value = [
                {
                    "status": "completed",
                    "block_code": "block-1",
                    "resolved_metrics": {"soil_moisture": 77.0},
                    "metric_sources": {},
                    "satellite_sub_blocks": [],
                    "sensor_sub_blocks": [],
                }
            ]

            response = self.client.get(f"/api/farm-data/{self.farm_uuid}/detail/")

        self.assertEqual(response.status_code, 200)
        payload = response.json()["data"]
        self.assertEqual(payload["soil"]["resolved_metrics"]["soil_moisture"], 12.0)
        self.assertNotEqual(
            payload["soil"]["resolved_metrics"]["soil_moisture"],
            payload["soil"]["block_snapshots"][0]["resolved_metrics"]["soil_moisture"],
        )


class BuildAiFarmSnapshotTests(TestCase):
    def setUp(self):
        self.location = SoilLocation.objects.create(
            latitude="35.700000",
            longitude="51.400000",
            farm_boundary={"type": "Polygon", "coordinates": []},
        )
        self.weather = WeatherForecast.objects.create(
            location=self.location,
            forecast_date=date(2026, 4, 10),
            temperature_min=12.0,
            temperature_max=23.0,
            temperature_mean=18.0,
            precipitation=1.2,
            humidity_mean=52.0,
        )
        self.plant = PlantCatalogSnapshot.objects.create(backend_plant_id=201, name="ذرت")
        self.irrigation_method = IrrigationMethod.objects.create(name="تیپ")
        self.farm_uuid = uuid.uuid4()
        self.farm = SensorData.objects.create(
            farm_uuid=self.farm_uuid,
            center_location=self.location,
            weather_forecast=self.weather,
            irrigation_method=self.irrigation_method,
            sensor_payload={"sensor-1": {"soil_moisture": 30.0}},
        )
        assign_farm_plants_from_backend_ids(self.farm, [self.plant.backend_plant_id])

    def test_build_ai_farm_snapshot_returns_normalized_block_and_sub_block_metrics(self):
        with patch("farm_data.services.build_farmer_block_aggregated_snapshot") as aggregated_mock, patch(
            "farm_data.services.build_location_block_satellite_snapshots"
        ) as block_mock:
            aggregated_mock.return_value = {
                "status": "completed",
                "aggregation_strategy": "farmer_block_mean",
                "block_count": 1,
                "resolved_metrics": {"ndvi": 0.6, "soil_moisture": 24.0},
                "metric_sources": {"ndvi": {"type": "farmer_block"}, "soil_moisture": {"type": "farmer_block"}},
            }
            block_mock.return_value = [
                {
                    "status": "completed",
                    "block_code": "block-1",
                    "aggregation_strategy": "sub_block_mean",
                    "resolved_metrics": {"ndvi": 0.6, "soil_moisture": 24.0},
                    "metric_sources": {"ndvi": {"type": "satellite"}, "soil_moisture": {"type": "sensor"}},
                    "satellite_metrics": {"ndvi": 0.6},
                    "sensor_metrics": {"soil_moisture": 24.0},
                    "sub_block_count": 2,
                    "run_id": 91,
                    "cell_count": 8,
                    "temporal_extent": {"start_date": "2026-04-01", "end_date": "2026-04-30"},
                    "satellite_sub_blocks": [
                        {"sub_block_code": "cluster-a", "resolved_metrics": {"ndvi": 0.5}},
                        {"sub_block_code": "cluster-b", "resolved_metrics": {"ndvi": 0.7}},
                    ],
                    "sensor_sub_blocks": [
                        {"sub_block_code": "cluster-a", "resolved_metrics": {"soil_moisture": 20.0}},
                        {"sub_block_code": "cluster-b", "resolved_metrics": {"soil_moisture": 28.0}},
                    ],
                }
            ]

            snapshot = build_ai_farm_snapshot(str(self.farm_uuid))

        self.assertIsNotNone(snapshot)
        self.assertEqual(snapshot["farm_uuid"], str(self.farm_uuid))
        self.assertEqual(snapshot["aggregation_policy"]["sensor"], "cluster_mean_then_block_mean_then_farm_mean")
        self.assertEqual(snapshot["farm_metrics"]["resolved_metrics"]["soil_moisture"], 24.0)
        self.assertEqual(snapshot["block_metrics"][0]["block_code"], "block-1")
        self.assertEqual(snapshot["block_metrics"][0]["source_metadata"]["run_id"], 91)
        self.assertEqual(len(snapshot["sub_block_metrics"]), 2)
        self.assertEqual(snapshot["sub_block_metrics"][0]["source_metadata"]["satellite_present"], True)
        self.assertEqual(snapshot["sub_block_metrics"][0]["source_metadata"]["sensor_present"], True)
        self.assertEqual(snapshot["weather"]["source_metadata"]["scope"], "location_center_based")
        self.assertEqual(len(snapshot["plants"]), 1)
        self.assertEqual(snapshot["irrigation_method"]["details"]["name"], self.irrigation_method.name)

    def test_build_ai_farm_snapshot_without_explicit_subdivisions_uses_default_compatibility_shape(self):
        with patch("farm_data.services.build_farmer_block_aggregated_snapshot") as aggregated_mock, patch(
            "farm_data.services.build_location_block_satellite_snapshots"
        ) as block_mock:
            aggregated_mock.return_value = {
                "status": "completed",
                "aggregation_strategy": "farmer_block_mean",
                "block_count": 1,
                "resolved_metrics": {"ndvi": 0.55},
                "metric_sources": {"ndvi": {"type": "farmer_block"}},
            }
            block_mock.return_value = [
                {
                    "status": "completed",
                    "block_code": "",
                    "resolved_metrics": {"ndvi": 0.55},
                    "metric_sources": {"ndvi": {"type": "satellite"}},
                    "satellite_metrics": {"ndvi": 0.55},
                    "sensor_metrics": {},
                    "satellite_sub_blocks": [],
                    "sensor_sub_blocks": [],
                }
            ]

            snapshot = build_ai_farm_snapshot(str(self.farm_uuid))

        self.assertEqual(snapshot["block_metrics"][0]["block_code"], "default-block")
        self.assertEqual(snapshot["sub_block_metrics"][0]["sub_block_code"], "default-sub-block")
        self.assertEqual(
            snapshot["sub_block_metrics"][0]["source_metadata"]["source"],
            "default_sub_block_compatibility",
        )
        self.assertEqual(
            snapshot["aggregation_policy"]["default_block_policy"],
            "1_main_block + 1_default_sub_block_when_missing",
        )

    def test_build_ai_farm_snapshot_handles_missing_sensor_data(self):
        self.farm.sensor_payload = None
        self.farm.save(update_fields=["sensor_payload"])

        with patch("farm_data.services.build_farmer_block_aggregated_snapshot") as aggregated_mock, patch(
            "farm_data.services.build_location_block_satellite_snapshots"
        ) as block_mock:
            aggregated_mock.return_value = {
                "status": "completed",
                "aggregation_strategy": "farmer_block_mean",
                "block_count": 1,
                "resolved_metrics": {"ndvi": 0.49},
                "metric_sources": {"ndvi": {"type": "farmer_block"}},
            }
            block_mock.return_value = [
                {
                    "status": "completed",
                    "block_code": "block-1",
                    "resolved_metrics": {"ndvi": 0.49},
                    "metric_sources": {"ndvi": {"type": "satellite"}},
                    "satellite_metrics": {"ndvi": 0.49},
                    "sensor_metrics": {},
                    "satellite_sub_blocks": [],
                    "sensor_sub_blocks": [],
                }
            ]

            snapshot = build_ai_farm_snapshot(str(self.farm_uuid))

        self.assertEqual(snapshot["farm_metrics"]["resolved_metrics"], {"ndvi": 0.49})
        self.assertEqual(snapshot["block_metrics"][0]["sensor_metrics"], {})

    def test_build_ai_farm_snapshot_handles_missing_remote_sensing_data(self):
        with patch("farm_data.services.build_farmer_block_aggregated_snapshot") as aggregated_mock, patch(
            "farm_data.services.build_location_block_satellite_snapshots"
        ) as block_mock:
            aggregated_mock.return_value = {
                "status": "completed",
                "aggregation_strategy": "farmer_block_mean",
                "block_count": 1,
                "resolved_metrics": {"soil_moisture": 30.0},
                "metric_sources": {"soil_moisture": {"type": "farmer_block"}},
            }
            block_mock.return_value = [
                {
                    "status": "completed",
                    "block_code": "block-1",
                    "resolved_metrics": {"soil_moisture": 30.0},
                    "metric_sources": {"soil_moisture": {"type": "sensor"}},
                    "satellite_metrics": {},
                    "sensor_metrics": {"soil_moisture": 30.0},
                    "satellite_sub_blocks": [],
                    "sensor_sub_blocks": [
                        {"sub_block_code": "cluster-1", "resolved_metrics": {"soil_moisture": 30.0}}
                    ],
                }
            ]

            snapshot = build_ai_farm_snapshot(str(self.farm_uuid))

        self.assertEqual(snapshot["farm_metrics"]["resolved_metrics"]["soil_moisture"], 30.0)
        self.assertEqual(snapshot["block_metrics"][0]["satellite_metrics"], {})
        self.assertEqual(snapshot["sub_block_metrics"][0]["sensor_metrics"]["soil_moisture"], 30.0)


class FarmDataUpsertApiTests(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.location = SoilLocation.objects.create(
            latitude="35.710000",
            longitude="51.410000",
        )
        self.boundary = square_boundary_for_center(35.71, 51.41)
        self.weather = WeatherForecast.objects.create(
            location=self.location,
            forecast_date=date(2026, 4, 11),
            temperature_min=11.0,
            temperature_max=24.0,
            temperature_mean=17.5,
        )
        self.irrigation_method = IrrigationMethod.objects.create(name="بارانی")

    def test_post_creates_farm_data_with_explicit_farm_uuid(self):
        farm_uuid = uuid.uuid4()

        response = self.client.post(
            "/api/farm-data/",
            data={
                "farm_uuid": str(farm_uuid),
                "farm_boundary": self.boundary,
                "sensor_payload": {
                    "sensor-7-1": {
                        "soil_moisture": 31.2,
                        "nitrogen": 18.0,
                    }
                },
                "irrigation_method_id": self.irrigation_method.id,
            },
            format="json",
        )

        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.json()["data"]["farm_uuid"], str(farm_uuid))
        self.assertEqual(response.json()["data"]["center_location_id"], self.location.id)
        self.assertEqual(response.json()["data"]["weather_forecast_id"], self.weather.id)

        farm = SensorData.objects.get(farm_uuid=farm_uuid)
        self.assertEqual(farm.center_location_id, self.location.id)
        self.assertEqual(farm.weather_forecast_id, self.weather.id)
        self.assertEqual(farm.irrigation_method_id, self.irrigation_method.id)
        self.assertEqual(
            farm.sensor_payload["sensor-7-1"]["soil_moisture"],
            31.2,
        )
        device = Device.objects.get(farm=farm, sensor_name="sensor-7-1")
        self.assertEqual(device.location_id, self.location.id)
        self.assertEqual(device.payload["soil_moisture"], 31.2)
        self.assertIsNone(device.cluster_uuid)
        self.assertTrue(
            SensorParameter.objects.filter(sensor_key="sensor-7-1", code="soil_moisture").exists()
        )

    def test_post_auto_registers_new_sensor_without_manual_parameter_creation(self):
        farm_uuid = uuid.uuid4()

        response = self.client.post(
            "/api/farm-data/",
            data={
                "farm_uuid": str(farm_uuid),
                "farm_boundary": self.boundary,
                "sensor_payload": {
                    "canopy-sensor-v2": {
                        "leaf_wetness": 12.4,
                        "leaf_temperature": 21.6,
                        "disease_pressure_index": 0.41,
                    }
                },
            },
            format="json",
        )

        self.assertEqual(response.status_code, 201)
        self.assertTrue(
            SensorParameter.objects.filter(
                sensor_key="canopy-sensor-v2",
                code="leaf_wetness",
            ).exists()
        )
        detail_response = self.client.get(f"/api/farm-data/{farm_uuid}/detail/")
        self.assertEqual(detail_response.status_code, 200)
        schema = detail_response.json()["data"]["sensor_schema"]["canopy-sensor-v2"]
        self.assertCountEqual(
            [item["code"] for item in schema],
            ["disease_pressure_index", "leaf_temperature", "leaf_wetness"],
        )

    def test_post_syncs_device_rows_from_sensor_payload(self):
        farm_uuid = uuid.uuid4()
        cluster_uuid = uuid.uuid4()

        create_response = self.client.post(
            "/api/farm-data/",
            data={
                "farm_uuid": str(farm_uuid),
                "farm_boundary": self.boundary,
                "sensor_payload": {
                    "sensor-7-1": {
                        "cluster_uuid": str(cluster_uuid),
                        "soil_moisture": 31.2,
                    },
                    "leaf-sensor": {
                        "leaf_wetness": 10.0,
                    },
                },
            },
            format="json",
        )

        self.assertEqual(create_response.status_code, 201)
        farm = SensorData.objects.get(farm_uuid=farm_uuid)
        self.assertEqual(Device.objects.filter(farm=farm).count(), 2)
        soil_device = Device.objects.get(farm=farm, sensor_name="sensor-7-1")
        self.assertEqual(str(soil_device.cluster_uuid), str(cluster_uuid))
        self.assertEqual(soil_device.payload["soil_moisture"], 31.2)

        update_response = self.client.post(
            "/api/farm-data/",
            data={
                "farm_uuid": str(farm_uuid),
                "farm_boundary": self.boundary,
                "sensor_payload": {
                    "sensor-7-1": {
                        "cluster_uuid": str(cluster_uuid),
                        "soil_moisture": 33.8,
                        "nitrogen": 20.5,
                    },
                },
            },
            format="json",
        )

        self.assertEqual(update_response.status_code, 200)
        soil_device.refresh_from_db()
        self.assertEqual(soil_device.payload["soil_moisture"], 33.8)
        self.assertEqual(soil_device.payload["nitrogen"], 20.5)
        self.assertEqual(Device.objects.filter(farm=farm, sensor_name="leaf-sensor").count(), 1)

    def test_post_requires_farm_uuid_in_request_body(self):
        response = self.client.post(
            "/api/farm-data/",
            data={
                "farm_boundary": self.boundary,
                "sensor_payload": {"sensor-7-1": {"soil_moisture": 31.2}},
            },
            format="json",
        )

        self.assertEqual(response.status_code, 400)
        self.assertIn("farm_uuid", response.json()["data"])

    def test_post_creates_center_location_from_boundary_when_missing(self):
        farm_uuid = uuid.uuid4()

        response = self.client.post(
            "/api/farm-data/",
            data={
                "farm_uuid": str(farm_uuid),
                "farm_boundary": {
                    "corners": [
                        {"lat": 50.0, "lon": 50.0},
                        {"lat": 50.0, "lon": 50.02},
                        {"lat": 50.02, "lon": 50.02},
                        {"lat": 50.02, "lon": 50.0},
                    ]
                },
                "sensor_payload": {"sensor-7-1": {"soil_moisture": 40.0}},
            },
            format="json",
        )

        self.assertEqual(response.status_code, 201)
        farm = SensorData.objects.get(farm_uuid=farm_uuid)
        self.assertIsNotNone(farm.center_location_id)
        self.assertEqual(str(farm.center_location.latitude), "50.010000")
        self.assertEqual(str(farm.center_location.longitude), "50.010000")
        self.assertIsNone(farm.weather_forecast_id)
        self.assertEqual(farm.center_location.input_block_count, 1)
        self.assertEqual(len(farm.center_location.block_layout["blocks"]), 1)
        subdivision = BlockSubdivision.objects.get(soil_location=farm.center_location, block_code="block-1")
        self.assertGreater(subdivision.grid_point_count, 0)
        self.assertEqual(subdivision.grid_point_count, subdivision.centroid_count)

    def test_post_persists_requested_block_count_on_center_location(self):
        farm_uuid = uuid.uuid4()

        response = self.client.post(
            "/api/farm-data/",
            data={
                "farm_uuid": str(farm_uuid),
                "farm_boundary": self.boundary,
                "block_count": 3,
                "sensor_payload": {"sensor-7-1": {"soil_moisture": 40.0}},
            },
            format="json",
        )

        self.assertEqual(response.status_code, 201)
        farm = SensorData.objects.get(farm_uuid=farm_uuid)
        self.assertEqual(farm.center_location.input_block_count, 3)
        self.assertEqual(len(farm.center_location.block_layout["blocks"]), 3)
        self.assertFalse(
            BlockSubdivision.objects.filter(soil_location=farm.center_location).exists()
        )

    def test_resolve_center_location_runs_subdivision_only_on_creation(self):
        boundary = square_boundary_for_center(35.75, 51.45)

        first_location = resolve_center_location_from_boundary(boundary, block_count=1)
        first_subdivision = BlockSubdivision.objects.get(
            soil_location=first_location,
            block_code="block-1",
        )

        second_location = resolve_center_location_from_boundary(boundary, block_count=1)

        self.assertEqual(first_location.id, second_location.id)
        self.assertEqual(
            BlockSubdivision.objects.filter(
                soil_location=second_location,
                block_code="block-1",
            ).count(),
            1,
        )
        self.assertEqual(
            BlockSubdivision.objects.get(
                soil_location=second_location,
                block_code="block-1",
            ).id,
            first_subdivision.id,
        )

    def test_resolve_center_location_uses_geometric_centroid_for_concave_polygon(self):
        location = resolve_center_location_from_boundary(
            {
                "corners": [
                    {"lat": 0.0, "lon": 0.0},
                    {"lat": 0.0, "lon": 4.0},
                    {"lat": 4.0, "lon": 4.0},
                    {"lat": 4.0, "lon": 0.0},
                    {"lat": 1.0, "lon": 0.0},
                    {"lat": 1.0, "lon": 3.0},
                    {"lat": 3.0, "lon": 3.0},
                    {"lat": 3.0, "lon": 1.0},
                    {"lat": 0.0, "lon": 1.0},
                ]
            }
        )

        self.assertEqual(str(location.latitude), "2.078947")
        self.assertEqual(str(location.longitude), "2.078947")

    def test_post_keeps_missing_location_without_external_sync(self):
        missing_boundary = square_boundary_for_center(36.0, 52.0)
        farm_uuid = uuid.uuid4()

        response = self.client.post(
            "/api/farm-data/",
            data={
                "farm_uuid": str(farm_uuid),
                "farm_boundary": missing_boundary,
                "sensor_payload": {"sensor-7-1": {"soil_moisture": 44.0}},
            },
            format="json",
        )

        self.assertEqual(response.status_code, 201)
        farm = SensorData.objects.get(farm_uuid=farm_uuid)
        self.assertIsNone(farm.weather_forecast_id)
