"""Tests for analyse_availability functions. @copyright NHCarrigan @license Naomi's Public License @author Naomi Carrigan """ import sys from pathlib import Path import pytest # Add the cohort directory to the path for imports sys.path.insert(0, str(Path(__file__).parent.parent / "cohort")) from analyse_availability import ( analyze_applicant_availability, get_utc_blocks_for_hour, local_hour_to_utc, parse_time_slots, parse_utc_offset, ) class TestParseUtcOffset: """Tests for parse_utc_offset function.""" def test_positive_offset(self): """Should parse positive UTC offset.""" assert parse_utc_offset("Europe/London (UTC+0)") == 0 assert parse_utc_offset("Europe/Paris (UTC+1)") == 1 assert parse_utc_offset("Asia/Tokyo (UTC+9)") == 9 def test_negative_offset(self): """Should parse negative UTC offset.""" assert parse_utc_offset("America/New_York (UTC-5)") == -5 assert parse_utc_offset("America/Los_Angeles (UTC-8)") == -8 def test_offset_with_minutes(self): """Should parse offset with minutes component.""" assert parse_utc_offset("Asia/Kolkata (UTC+5:30)") == 5.5 assert parse_utc_offset("Asia/Kathmandu (UTC+5:45)") == 5.75 def test_negative_offset_with_minutes(self): """Should parse negative offset with minutes.""" assert parse_utc_offset("Canada/Newfoundland (UTC-3:30)") == -3.5 def test_no_match_returns_zero(self): """Should return 0 when no UTC offset found.""" assert parse_utc_offset("Unknown/Timezone") == 0 assert parse_utc_offset("") == 0 class TestParseTimeSlots: """Tests for parse_time_slots function.""" def test_single_slot(self): """Should parse a single time slot.""" result = parse_time_slots("17:00-18:00") assert result == [(17, 18)] def test_multiple_slots(self): """Should parse multiple time slots separated by semicolon.""" result = parse_time_slots("07:00-08:00; 19:00-20:00") assert result == [(7, 8), (19, 20)] def test_na_values(self): """Should return empty list for N/A values.""" assert parse_time_slots("N/A") == [] assert parse_time_slots("na") == [] assert parse_time_slots("") == [] def test_wider_time_range(self): """Should parse wider time ranges.""" result = parse_time_slots("09:00-17:00") assert result == [(9, 17)] def test_midnight_crossing_slots(self): """Should parse time slots that approach midnight.""" result = parse_time_slots("22:00-23:00") assert result == [(22, 23)] class TestLocalHourToUtc: """Tests for local_hour_to_utc function.""" def test_positive_offset(self): """Should convert local hour with positive UTC offset.""" assert local_hour_to_utc(12, 1) == 11 # Noon in UTC+1 -> 11:00 UTC assert local_hour_to_utc(0, 9) == 15 # Midnight in UTC+9 -> 15:00 UTC def test_negative_offset(self): """Should convert local hour with negative UTC offset.""" assert local_hour_to_utc(12, -5) == 17 # Noon in UTC-5 -> 17:00 UTC assert local_hour_to_utc(20, -8) == 4 # 8pm in UTC-8 -> 4:00 UTC (next day) def test_wrapping_around_midnight(self): """Should wrap around correctly at midnight.""" assert local_hour_to_utc(1, 5) == 20 # 1am in UTC+5 -> 20:00 UTC (prev day) assert local_hour_to_utc(23, -3) == 2 # 11pm in UTC-3 -> 2:00 UTC (next day) def test_zero_offset(self): """Should return same hour for zero offset.""" assert local_hour_to_utc(15, 0) == 15 class TestGetUtcBlocksForHour: """Tests for get_utc_blocks_for_hour function.""" def test_mornings_block(self): """Should return mornings for 6-11 UTC.""" assert "mornings" in get_utc_blocks_for_hour(6) assert "mornings" in get_utc_blocks_for_hour(9) assert "mornings" in get_utc_blocks_for_hour(11) assert "mornings" not in get_utc_blocks_for_hour(12) def test_afternoons_block(self): """Should return afternoons for 12-17 UTC.""" assert "afternoons" in get_utc_blocks_for_hour(12) assert "afternoons" in get_utc_blocks_for_hour(15) assert "afternoons" in get_utc_blocks_for_hour(17) assert "afternoons" not in get_utc_blocks_for_hour(18) def test_evenings_block(self): """Should return evenings for 18-23 UTC.""" assert "evenings" in get_utc_blocks_for_hour(18) assert "evenings" in get_utc_blocks_for_hour(21) assert "evenings" in get_utc_blocks_for_hour(23) assert "evenings" not in get_utc_blocks_for_hour(0) def test_nights_block(self): """Should return nights for 0-5 UTC.""" assert "nights" in get_utc_blocks_for_hour(0) assert "nights" in get_utc_blocks_for_hour(3) assert "nights" in get_utc_blocks_for_hour(5) assert "nights" not in get_utc_blocks_for_hour(6) class TestAnalyzeApplicantAvailability: """Tests for analyze_applicant_availability function.""" def test_basic_availability(self): """Should analyze basic availability correctly.""" day_slots = { "Monday": [(9, 12)], "Tuesday": [(9, 12)], "Wednesday": [(9, 12)], "Thursday": [], "Friday": [], "Saturday": [], "Sunday": [], } result = analyze_applicant_availability("UTC (UTC+0)", day_slots) assert result["utc_offset"] == 0 assert "mornings" in result["available_blocks"] def test_no_availability(self): """Should return empty blocks when no availability.""" day_slots = { "Monday": [], "Tuesday": [], "Wednesday": [], "Thursday": [], "Friday": [], "Saturday": [], "Sunday": [], } result = analyze_applicant_availability("UTC (UTC+0)", day_slots) assert result["available_blocks"] == [] assert result["total_unique_utc_hours"] == 0 def test_timezone_conversion(self): """Should correctly convert timezones.""" day_slots = { "Monday": [(17, 20)], # 5pm-8pm local "Tuesday": [(17, 20)], "Wednesday": [(17, 20)], "Thursday": [], "Friday": [], "Saturday": [], "Sunday": [], } result = analyze_applicant_availability( "America/New_York (UTC-5)", day_slots ) assert result["utc_offset"] == -5 # 5pm-8pm in UTC-5 = 22:00-01:00 UTC -> evenings/nights assert "evenings" in result["available_blocks"] def test_block_count_threshold(self): """Should only include blocks with 3+ occurrences.""" day_slots = { "Monday": [(9, 10)], # Only 1 hour "Tuesday": [(9, 10)], # Only 1 hour "Wednesday": [], "Thursday": [], "Friday": [], "Saturday": [], "Sunday": [], } result = analyze_applicant_availability("UTC (UTC+0)", day_slots) # Only 2 hours, threshold is 3 assert "mornings" not in result["available_blocks"]