""" Tests for the unified recommendation scoring (B3). Covers the shared scorer directly, the dict-based alias entry point, and the /api/v1/stats/recommendation endpoint that now delegates to it. """ import pytest from fastapi.testclient import TestClient from backend.main import app from services.recommendation_service import ( HIGH_PRIORITY_TEXT, NORMAL_PRIORITY_TEXT, get_statistical_recommendation, score_recommendation, ) client = TestClient(app) class TestScoreRecommendation: """Weights and threshold of the shared scorer.""" def test_empty_case_scores_zero(self): result = score_recommendation() assert result['score'] == 0 assert result['priority'] == 'normal' assert result['recommendation'] == NORMAL_PRIORITY_TEXT def test_young_child(self): assert score_recommendation(age=8)['score'] == 20 def test_age_at_threshold_not_counted(self): assert score_recommendation(age=12)['score'] == 0 def test_long_elapsed(self): assert score_recommendation(elapsed_hours=12)['score'] == 20 def test_short_elapsed_not_counted(self): assert score_recommendation(elapsed_hours=11)['score'] == 0 def test_risky_terrain(self): assert score_recommendation(terrain='лес')['score'] == 15 def test_adverse_weather(self): assert score_recommendation(weather='дождь')['score'] == 15 def test_multiple_health_flags(self): assert score_recommendation(health_flags=['эпилепсия', 'РАС'])['score'] == 15 def test_single_health_flag_not_counted(self): assert score_recommendation(health_flags=['эпилепсия'])['score'] == 0 def test_terrain_matches_as_substring(self): """Substring match — the router previously required an exact match.""" assert score_recommendation(terrain='смешанный лес')['score'] == 15 def test_weather_matches_as_substring(self): assert score_recommendation(weather='сильный дождь')['score'] == 15 def test_terrain_case_insensitive(self): assert score_recommendation(terrain='ЛЕС')['score'] == 15 def test_unknown_terrain_scores_zero(self): assert score_recommendation(terrain='поле')['score'] == 0 def test_high_priority_at_threshold(self): result = score_recommendation(age=8, elapsed_hours=14) assert result['score'] == 40 assert result['priority'] == 'high' assert result['recommendation'] == HIGH_PRIORITY_TEXT def test_just_below_threshold_is_normal(self): result = score_recommendation(age=8, terrain='лес') assert result['score'] == 35 assert result['priority'] == 'normal' def test_all_factors(self): result = score_recommendation( age=6, elapsed_hours=24, terrain='болото', weather='туман', health_flags=['РАС', 'эпилепсия'], ) assert result['score'] == 85 assert result['priority'] == 'high' class TestGetStatisticalRecommendation: """Dict entry point and its field aliases.""" def test_age_alias(self): assert get_statistical_recommendation({'age_years': 8})['score'] == 20 def test_age_preferred_over_alias(self): assert get_statistical_recommendation({'age': 8, 'age_years': 30})['score'] == 20 def test_terrain_alias(self): assert get_statistical_recommendation({'terrain_primary': 'лес'})['score'] == 15 def test_weather_alias(self): assert get_statistical_recommendation({'precipitation': 'снег'})['score'] == 15 def test_health_flags_counted(self): payload = {'health_flags': ['РАС', 'эпилепсия']} assert get_statistical_recommendation(payload)['score'] == 15 def test_missing_keys_are_safe(self): assert get_statistical_recommendation({})['score'] == 0 def test_none_values_are_safe(self): payload = {'age': None, 'elapsed_hours': None, 'terrain': None, 'weather': None} assert get_statistical_recommendation(payload)['score'] == 0 class TestRecommendationEndpoint: """The endpoint keeps its response contract while delegating.""" def test_returns_score_and_recommendation(self): response = client.post( '/api/v1/stats/recommendation', json={'age': 8, 'elapsed_hours': 14}, ) assert response.status_code == 200 body = response.json() assert body['score'] == 40 assert body['recommendation'] == HIGH_PRIORITY_TEXT def test_normal_priority_case(self): response = client.post('/api/v1/stats/recommendation', json={'age': 30}) assert response.status_code == 200 body = response.json() assert body['score'] == 0 assert body['recommendation'] == NORMAL_PRIORITY_TEXT def test_empty_payload_accepted(self): response = client.post('/api/v1/stats/recommendation', json={}) assert response.status_code == 200 assert response.json()['score'] == 0 def test_endpoint_matches_shared_scorer(self): payload = { 'age': 6, 'elapsed_hours': 24, 'terrain_primary': 'болото', 'weather': 'туман', 'health_flags': ['РАС', 'эпилепсия'], } response = client.post('/api/v1/stats/recommendation', json=payload) assert response.status_code == 200 expected = score_recommendation( age=payload['age'], elapsed_hours=payload['elapsed_hours'], terrain=payload['terrain_primary'], weather=payload['weather'], health_flags=payload['health_flags'], ) assert response.json()['score'] == expected['score'] assert response.json()['recommendation'] == expected['recommendation']