""" Tests for distance_service.py Tests the distance calculation formulas and prior probabilities based on §9 ВЕКТОР-контекст.md specifications. """ import pytest from services.distance_service import ( calculate_max_distance, get_base_speed, get_terrain_coefficient, get_time_of_day_coefficient, get_weather_coefficient, get_distance_priors, get_distance_zone, get_distance_statistics ) class TestCalculateMaxDistance: """Test the main distance calculation function. Ожидания соответствуют каноничным displacement-скоростям B5 (3e2692f): get_base_speed 0.3–2.5 км/ч по априорам ПСО «Экстремум» (94% в 3 км). Контрольная точка B5: bike 8yo 2h лес = 5.4 км. """ def test_boy_10_years_4_hours_forest_rain(self): """ Мальчик 10 лет, 4 часа, лес, дождь. Пересчитано под канон B5: НормС(10)=1.5 Distance = 4 * 1.5 * 0.5 * 0.8 * 1.0 * 0.8 = 1.92 km """ case_data = { 'age': 10, 'elapsed_hours': 4.0, 'terrain_primary': 'лес', 'time_of_day': 'день', 'weather': 'дождь' } distance = calculate_max_distance(case_data) assert distance == pytest.approx(1.92, rel=0.01) def test_young_child_short_time(self): """Test for young child (3 years) with short elapsed time.""" case_data = { 'age': 3, 'elapsed_hours': 1.0, 'terrain_primary': 'лес', 'time_of_day': 'день', 'weather': 'нет' } distance = calculate_max_distance(case_data) # 1 * 0.7 * 0.5 * 0.95 * 1.0 * 1.0 = 0.33 km assert distance == pytest.approx(0.33, rel=0.01) def test_teenager_long_time_road(self): """Test for teenager on road with longer elapsed time.""" case_data = { 'age': 15, 'elapsed_hours': 6.0, 'terrain_primary': 'дорога', 'time_of_day': 'день', 'weather': 'нет' } distance = calculate_max_distance(case_data) # 6 * 2.0 * 0.8 * 0.7 * 1.0 * 1.0 = 6.72 km assert distance == pytest.approx(6.72, rel=0.01) def test_elderly_night_swamp(self): """Test for elderly person at night in swamp.""" case_data = { 'age': 70, 'elapsed_hours': 3.0, 'terrain_primary': 'болото', 'time_of_day': 'ночь', 'weather': 'туман' } distance = calculate_max_distance(case_data) # 3 * 1.5 * 0.2 * 0.85 * 0.0(ночь) * 0.7 = 0.0 км — # методичка §12.3.5: без фонаря ночью движение = 0 assert distance == pytest.approx(0.0, rel=0.01) def test_adult_heavy_rain_field(self): """Test for adult in heavy rain on field.""" case_data = { 'age': 35, 'elapsed_hours': 2.0, 'terrain_primary': 'поле', 'time_of_day': 'день', 'weather': 'ливень' } distance = calculate_max_distance(case_data) # 2 * 2.5 * 0.9 * 0.9 * 1.0 * 0.6 = 2.43 km assert distance == pytest.approx(2.43, rel=0.01) def test_bike_8yo_canonical_b5(self): """Контрольная точка B5: bike 8yo 2h лес = 5.4 км (СУ bike ×5).""" case_data = { 'age': 8, 'elapsed_hours': 2.0, 'terrain_primary': 'лес', 'time_of_day': 'день', 'weather': 'нет', 'has_transport': 'bike', 'diagnosis_type': [], } distance = calculate_max_distance(case_data) assert distance == pytest.approx(5.4, rel=0.01) class TestBaseSpeed: """Test НормС (base speed) by age — каноничные displacement-скорости B5.""" def test_infant(self): assert get_base_speed(1) == 0.3 assert get_base_speed(2) == 0.3 def test_preschool(self): assert get_base_speed(3) == 0.7 assert get_base_speed(5) == 0.7 def test_young_child(self): assert get_base_speed(6) == 1.2 assert get_base_speed(8) == 1.2 def test_preteen(self): assert get_base_speed(10) == 1.5 assert get_base_speed(12) == 1.5 def test_teenager(self): assert get_base_speed(13) == 2.0 assert get_base_speed(15) == 2.0 assert get_base_speed(16) == 2.5 assert get_base_speed(17) == 2.5 def test_adult(self): assert get_base_speed(25) == 2.5 assert get_base_speed(50) == 2.5 assert get_base_speed(64) == 2.5 def test_elderly(self): assert get_base_speed(65) == 1.5 assert get_base_speed(80) == 1.5 class TestTerrainCoefficient: """Test СП (terrain coefficient).""" def test_road_terrain(self): assert get_terrain_coefficient('дорога') == 0.8 assert get_terrain_coefficient('лесная дорога') == 0.8 assert get_terrain_coefficient('тропа') == 0.8 def test_forest_terrain(self): assert get_terrain_coefficient('лес') == 0.5 assert get_terrain_coefficient('простой лес') == 0.5 assert get_terrain_coefficient('сложный лес') == 0.25 assert get_terrain_coefficient('густой лес') == 0.25 def test_open_terrain(self): assert get_terrain_coefficient('поле') == 0.9 assert get_terrain_coefficient('луг') == 0.9 def test_difficult_terrain(self): assert get_terrain_coefficient('болото') == 0.2 assert get_terrain_coefficient('горы') == 0.3 assert get_terrain_coefficient('овраг') == 0.3 def test_urban_terrain(self): assert get_terrain_coefficient('город') == 1.0 assert get_terrain_coefficient('населённый пункт') == 1.0 def test_unknown_terrain(self): assert get_terrain_coefficient('неизвестно') == 0.5 class TestTimeOfDayCoefficient: """Test ВВС (time of day coefficient).""" def test_day(self): assert get_time_of_day_coefficient('день') == 1.0 def test_night(self): # Методичка §12.3.5: ночь = 0 (без фонаря движение прекращается) assert get_time_of_day_coefficient('ночь') == 0.0 def test_twilight(self): # Методичка §12.3.3: за час до заката скорость 50% от обычной assert get_time_of_day_coefficient('сумерки') == 0.5 assert get_time_of_day_coefficient('вечер') == 0.5 class TestWeatherCoefficient: """Test ВП (weather coefficient).""" def test_clear_weather(self): assert get_weather_coefficient('нет') == 1.0 assert get_weather_coefficient('ясно') == 1.0 def test_rain(self): assert get_weather_coefficient('дождь') == 0.8 assert get_weather_coefficient('ливень') == 0.6 assert get_weather_coefficient('сильный дождь') == 0.6 def test_fog(self): assert get_weather_coefficient('туман') == 0.7 def test_snow(self): assert get_weather_coefficient('снег') == 0.6 assert get_weather_coefficient('метель') == 0.6 def test_heat(self): assert get_weather_coefficient('жара') == 0.8 class TestDistancePriors: """Test get_distance_priors() - априорные вероятности зон.""" def test_young_child_priors(self): """Children under 8 stay close.""" priors = get_distance_priors(5) assert priors['0_500m'] == 0.45 assert priors['500_1500m'] == 0.35 assert priors['1500_2500m'] == 0.15 assert sum(priors.values()) == pytest.approx(1.0, rel=0.01) def test_preteen_priors(self): """Children 8-12 have more even distribution.""" priors = get_distance_priors(10) assert priors['0_500m'] == 0.28 assert priors['500_1500m'] == 0.25 assert priors['1500_2500m'] == 0.22 assert sum(priors.values()) == pytest.approx(1.0, rel=0.01) def test_teenager_priors(self): """Teenagers can go farther.""" priors = get_distance_priors(15) assert priors['1500_2500m'] == 0.25 assert priors['5000_plus'] == 0.08 assert sum(priors.values()) == pytest.approx(1.0, rel=0.01) def test_adult_priors(self): """Adults have highest far-distance probability.""" priors = get_distance_priors(35) assert priors['0_500m'] == 0.12 assert priors['5000_plus'] == 0.13 assert sum(priors.values()) == pytest.approx(1.0, rel=0.01) def test_elderly_priors(self): """Elderly stay closer like young children.""" priors = get_distance_priors(70) assert priors['0_500m'] == 0.35 assert priors['5000_plus'] == 0.02 assert sum(priors.values()) == pytest.approx(1.0, rel=0.01) class TestDistanceZone: """Test get_distance_zone() classification.""" def test_zone_classification(self): assert get_distance_zone(0.3) == '0_500m' assert get_distance_zone(0.5) == '500_1500m' assert get_distance_zone(1.0) == '500_1500m' assert get_distance_zone(1.5) == '1500_2500m' assert get_distance_zone(2.5) == '2500_3500m' assert get_distance_zone(4.0) == '3500_5000m' assert get_distance_zone(6.0) == '5000_plus' class TestDistanceStatistics: """Test get_distance_statistics() comprehensive output.""" def test_statistics_structure(self): stats = get_distance_statistics( age_years=10, elapsed_hours=4.0, terrain='лес' ) assert 'max_distance_km' in stats assert 'current_zone' in stats assert 'zone_probability' in stats assert 'all_priors' in stats assert 'base_speed_kmh' in stats assert 'terrain_coefficient' in stats def test_statistics_values(self): stats = get_distance_statistics( age_years=10, elapsed_hours=4.0, terrain='лес' ) assert stats['base_speed_kmh'] == 1.5 assert stats['terrain_coefficient'] == 0.5 assert stats['max_distance_km'] > 0 assert 0 <= stats['zone_probability'] <= 1.0 class TestFatigueCoefficient: """Test СУТ (fatigue coefficient) behavior.""" def test_fatigue_progression(self): """Fatigue increases with time (5% per hour).""" case_1h = { 'age': 30, 'elapsed_hours': 1.0, 'terrain_primary': 'поле', 'time_of_day': 'день', 'weather': 'нет' } dist_1h = calculate_max_distance(case_1h) case_5h = case_1h.copy() case_5h['elapsed_hours'] = 5.0 dist_5h = calculate_max_distance(case_5h) case_10h = case_1h.copy() case_10h['elapsed_hours'] = 10.0 dist_10h = calculate_max_distance(case_10h) assert dist_5h < dist_1h * 5 assert dist_10h < dist_5h * 2 def test_fatigue_minimum(self): """Fatigue coefficient has minimum of 0.3.""" case_data = { 'age': 30, 'elapsed_hours': 20.0, 'terrain_primary': 'поле', 'time_of_day': 'день', 'weather': 'нет' } distance = calculate_max_distance(case_data) # 20 * 2.5 * 0.9 * 0.3 * 1.0 * 1.0 = 13.5 assert distance == pytest.approx(13.5, rel=0.01)