"""Unit tests for INCUBAZPRIMARIA model_value rounding and labels.""" from __future__ import annotations import unittest from datetime import date from decimal import Decimal from types import SimpleNamespace from unittest.mock import patch from pipeline.errors import NoModelConfiguredError, PipelineError from pipeline.stages.disease import ( _DayRecords, _format_model_value, _incub_label, _resolve_day_fase, find_advice_anmod_ids, resolve_disease_forecast, resolve_disease_forecast_by_anmod, ) class FormatModelValueTests(unittest.TestCase): def test_long_float_rounds_to_two_decimals(self) -> None: self.assertEqual(_format_model_value(40.123456789), "40.12") def test_long_decimal_rounds_to_two_decimals(self) -> None: self.assertEqual(_format_model_value(Decimal("40.123456789")), "40.12") def test_trailing_zeros_stripped(self) -> None: self.assertEqual(_format_model_value(40.30), "40.3") self.assertEqual(_format_model_value(40.0), "40") self.assertEqual(_format_model_value(Decimal("40.10")), "40.1") def test_none_and_empty_return_none(self) -> None: self.assertIsNone(_format_model_value(None)) self.assertIsNone(_format_model_value(" ")) def test_non_numeric_passthrough(self) -> None: self.assertEqual(_format_model_value("n/a"), "n/a") class IncubLabelTests(unittest.TestCase): def test_picks_max_raw_value_then_rounds_suffix(self) -> None: self.assertEqual( _incub_label([ ("INCUBAZPRIMARIA", 40.12), ("INCUBAZPRIMARIA", 40.129), ("INCUBAZPRIMARIA", 39.99), ]), "INCUBAZPRIMARIA_40.13", ) def test_keeps_index_of_winning_row(self) -> None: self.assertEqual( _incub_label([("INCUBAZPRIMARIA2", 80.7)]), "INCUBAZPRIMARIA2_80.7", ) def test_higher_percentage_beats_higher_index(self) -> None: self.assertEqual( _incub_label([ ("INCUBAZPRIMARIA2", 40), ("INCUBAZPRIMARIA1", 90), ]), "INCUBAZPRIMARIA1_90", ) def test_tie_keeps_higher_index(self) -> None: self.assertEqual( _incub_label([ ("INCUBAZPRIMARIA1", 80.7), ("INCUBAZPRIMARIA2", 80.7), ]), "INCUBAZPRIMARIA2_80.7", ) def test_missing_index_loses_tie(self) -> None: self.assertEqual( _incub_label([ ("INCUBAZPRIMARIA", 40), ("INCUBAZPRIMARIA2", 40), ]), "INCUBAZPRIMARIA2_40", ) def test_space_or_underscore_is_not_an_index(self) -> None: self.assertEqual( _incub_label([("INCUBAZPRIMARIA 2", 40), ("INCUBAZPRIMARIA_2", 30)]), "INCUBAZPRIMARIA_40", ) def test_empty_or_non_numeric_falls_back(self) -> None: self.assertEqual(_incub_label([]), "INCUBAZPRIMARIA") self.assertEqual( _incub_label([("INCUBAZPRIMARIA", None), ("INCUBAZPRIMARIA2", "n/a")]), "INCUBAZPRIMARIA", ) def test_day_keeps_each_index_and_fase5(self) -> None: records = _DayRecords( descriptions={"INCUBAZPRIMARIA1", "INCUBAZPRIMARIA2", "FASE5", "FASE2"}, incub_rows=[ ("INCUBAZPRIMARIA1", 90), ("INCUBAZPRIMARIA2", 40), ], ) self.assertEqual( _resolve_day_fase(records), ["INCUBAZPRIMARIA1_90", "INCUBAZPRIMARIA2_40", "FASE5"], ) def test_incubation_only_keeps_highest_value_per_index(self) -> None: records = _DayRecords( descriptions={"INCUBAZPRIMARIA2"}, incub_rows=[ ("INCUBAZPRIMARIA2", 40), ("INCUBAZPRIMARIA2", 80.7), ], ) self.assertEqual(_resolve_day_fase(records), ["INCUBAZPRIMARIA2_80.7"]) def test_fase2_without_incubation(self) -> None: records = _DayRecords(descriptions={"FASE2"}) self.assertEqual(_resolve_day_fase(records), ["FASE2"]) def test_empty_day_is_none(self) -> None: self.assertIsNone(_resolve_day_fase(_DayRecords())) def _model(anmod_id: int, name: str = "PERONOSPORA", advice: int = 1) -> SimpleNamespace: return SimpleNamespace(anmod_id=anmod_id, anmod_model=name, anmod_AI_advice=advice) def _tmp_row(day: date, description: str, value: object, station: int = 7) -> SimpleNamespace: return SimpleNamespace( model_day=day, model_description=description, model_value=value, model_station=station, ) class AdviceModelLookupTests(unittest.TestCase): def test_keeps_every_advice_enabled_id_for_the_disease(self) -> None: rows = [ _model(20), _model(11, name="Peronospora"), _model(12, advice=0), _model(13, name="OIDIO"), ] with patch("pipeline.stages.disease.fetch_all", return_value=rows) as fetch: ids = find_advice_anmod_ids(object(), 4012, "PERONOSPORA") self.assertEqual(ids, [11, 20]) self.assertIn("anmod_AI_advice", fetch.call_args.args[1]) def test_disabled_only_match_is_not_configured(self) -> None: with patch("pipeline.stages.disease.fetch_all", return_value=[_model(12, advice=0)]): with self.assertRaises(NoModelConfiguredError): find_advice_anmod_ids(object(), 4012, "PERONOSPORA") def test_forecast_reads_model_description_for_that_anmod_id(self) -> None: as_of = date(2026, 6, 15) with patch( "pipeline.stages.disease.fetch_all", return_value=[_tmp_row(as_of, "INCUBAZPRIMARIA2", 80.7)], ) as fetch: forecast = resolve_disease_forecast_by_anmod( object(), anmod_id=11, disease_name="PERONOSPORA", disease_english="downy mildew", as_of=as_of, fallback_station=None, ) sql, params = fetch.call_args.args[1], fetch.call_args.args[2] self.assertIn("WHERE model_model = ?", sql) self.assertIn("model_description", sql) self.assertEqual(params[0], 11) self.assertEqual(forecast.anmod_id, 11) self.assertEqual(forecast.forecasts_by_day[as_of], ["INCUBAZPRIMARIA2_80.7"]) def test_single_enabled_model_is_used_for_the_forecast(self) -> None: as_of = date(2026, 6, 15) with patch( "pipeline.stages.disease.fetch_all", side_effect=[[_model(11)], [_tmp_row(as_of, "FASE5", None, station=4)]], ) as fetch: forecast = resolve_disease_forecast( object(), 4012, "PERONOSPORA", "downy mildew", as_of, None, ) self.assertEqual(forecast.anmod_id, 11) self.assertEqual(forecast.forecasts_by_day[as_of], ["FASE5"]) self.assertEqual(fetch.call_args_list[1].args[2][0], 11) def test_several_enabled_models_are_not_collapsed_to_the_first(self) -> None: rows = [_model(11), _model(20)] with patch("pipeline.stages.disease.fetch_all", return_value=rows): with self.assertRaises(PipelineError): resolve_disease_forecast( object(), 4012, "PERONOSPORA", "downy mildew", date(2026, 6, 15), None, ) if __name__ == "__main__": unittest.main()