#!/usr/bin/env python3 """Tests for muhurta.py — Panchanga calculation, auspicious/inauspicious periods.""" from __future__ import annotations import sys, os, pytest SCRIPTS = os.path.join(os.path.dirname(__file__), '..', 'scripts') if SCRIPTS not in sys.path: sys.path.insert(0, SCRIPTS) from muhurta import ( calc_tithi, calc_nakshatra_from_lon, calc_yoga, calc_karana, calc_vara, calc_hora, calc_abhijit_muhurta, calc_panchanga, check_activity_muhurta, muhurta_full_report, calc_daytime_inauspicious_periods, panchanga_range_report, muhurta_range_search, calc_sunrise_sunset_local, calc_panchanga_end_times, build_muhurta_sidecar, classify_vrata_tags, classify_panchanga_condition_tags, calc_choghadiya_windows, calc_hora_windows, TITHI_NAMES, TITHI_QUALITY, NAKSHATRAS, NAKSHATRA_TYPE, YOGA_NAMES, YOGA_QUALITY, KARANA_NAMES, KARANA_QUALITY, VARA_LORDS, ACTIVITY_RULES, ) # ── Tithi Tests ───────────────────────────────────────────────────── class TestTithi: def test_new_moon_tithi_30(self): # Moon-Sun diff = 0 → tithi 1 result = calc_tithi(0.0, 0.0) assert result['tithi_num'] == 1 def test_full_moon_tithi_15(self): # Moon-Sun diff = 180° → tithi 16 (Krishna 1) result = calc_tithi(0.0, 180.0) assert result['tithi_num'] == 16 # Krishna Pratipada def test_shukla_paksha(self): result = calc_tithi(0.0, 60.0) # 60/12=5+1=6 assert result['paksha'] == 'Shukla' def test_krishna_paksha(self): result = calc_tithi(0.0, 210.0) # 210/12=17+1=18 assert result['paksha'] == 'Krishna' def test_purnima_name(self): result = calc_tithi(0.0, 174.0) # ~15th tithi if result['tithi_num'] == 15: assert result['name'] == 'Purnima' def test_amavasya_name(self): result = calc_tithi(0.0, 354.0) # ~30th tithi if result['tithi_num'] == 30: assert result['name'] == 'Amavasya' @pytest.mark.parametrize("diff,tithi", [(0, 1), (12, 2), (24, 3), (168, 15), (180, 16), (348, 30)]) def test_tithi_from_diff(self, diff, tithi): result = calc_tithi(0.0, float(diff)) assert result['tithi_num'] == tithi def test_tithi_quality_present(self): result = calc_tithi(0.0, 60.0) assert result['quality'] in ('subha', 'asubha', 'mixed') # ── Nakshatra from Longitude Tests ────────────────────────────────── class TestNakshatraFromLon: def test_ashwini_at_0(self): result = calc_nakshatra_from_lon(0.0) assert result['nakshatra'] == 'Ashwini' def test_revati_near_360(self): result = calc_nakshatra_from_lon(355.0) assert result['nakshatra'] == 'Revati' def test_pada_range(self): for lon in [1.0, 5.0, 8.0, 11.0]: result = calc_nakshatra_from_lon(lon) assert 1 <= result['pada'] <= 4 def test_27_nakshatras(self): assert len(NAKSHATRAS) == 27 def test_nakshatra_type(self): result = calc_nakshatra_from_lon(0.0) # Ashwini assert result['type'] == 'laghu' # ── Yoga Tests ────────────────────────────────────────────────────── class TestYoga: def test_vishkambha_at_low_sum(self): result = calc_yoga(0.0, 0.0) # sum=0 → idx 0 assert result['yoga'] == 'Vishkambha' def test_yoga_idx_range(self): result = calc_yoga(180.0, 180.0) assert 0 <= result['yoga_idx'] < 27 def test_yoga_quality_present(self): result = calc_yoga(45.0, 120.0) assert result['quality'] in ('subha', 'asubha', 'mixed') def test_27_yogas(self): assert len(YOGA_NAMES) == 27 def test_vaidhriti_asubha(self): assert YOGA_QUALITY['Vaidhriti'] == 'asubha' def test_priti_subha(self): assert YOGA_QUALITY['Priti'] == 'subha' # ── Karana Tests ──────────────────────────────────────────────────── class TestKarana: def test_kimstughna_first_half(self): # Moon-Sun diff near 0 → k_num=0 result = calc_karana(0.0, 1.0) assert result['karana'] == 'Kimstughna' def test_vishti_asubha(self): assert KARANA_QUALITY['Vishti'] == 'asubha' def test_karana_quality_present(self): result = calc_karana(0.0, 60.0) assert result['quality'] in ('subha', 'asubha', 'mixed') def test_11_karana_names(self): assert len(KARANA_NAMES) == 11 def test_is_vishti_flag(self): # Test when karana is Vishti result = calc_karana(0.0, 60.0) assert isinstance(result['is_vishti'], bool) # ── Vara Tests ────────────────────────────────────────────────────── class TestVara: def test_sunday(self): result = calc_vara(0) assert result['vara'] == 'Sunday' assert result['vara_lord'] == 'Sun' assert result['quality'] == 'asubha' def test_monday(self): result = calc_vara(1) assert result['vara_lord'] == 'Moon' assert result['quality'] == 'subha' def test_thursday_jupiter(self): result = calc_vara(4) assert result['vara_lord'] == 'Jupiter' def test_7_days(self): assert len(VARA_LORDS) == 7 @pytest.mark.parametrize("idx,expected_lord", [(0, 'Sun'), (1, 'Moon'), (2, 'Mars'), (3, 'Mercury'), (4, 'Jupiter'), (5, 'Venus'), (6, 'Saturn')]) def test_all_vara_lords(self, idx, expected_lord): result = calc_vara(idx) assert result['vara_lord'] == expected_lord # ── Hora Tests ────────────────────────────────────────────────────── class TestHora: def test_hora_lord_calculated(self): result = calc_hora(0, 6.0) # Sunday, 6h from sunrise assert result['hora_lord'] in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn') def test_hora_quality_present(self): result = calc_hora(0, 6.0) assert result['quality'] in ('subha', 'asubha', 'mixed') def test_hora_num_range(self): result = calc_hora(0, 6.0) assert 1 <= result['hora_num'] <= 24 # ── Abhijit Muhurta Tests ────────────────────────────────────────── class TestAbhijitMuhurta: def test_basic_structure(self): result = calc_abhijit_muhurta() assert 'duration_minutes' in result assert result['duration_minutes'] == 48 def test_wednesday_warning(self): result = calc_abhijit_muhurta() assert 'Wednesday' in result['warning'] def test_with_jd_values(self): result = calc_abhijit_muhurta(2460000.0, 2460000.5) assert 'abhijit_start_jd' in result assert 'abhijit_end_jd' in result class TestDaytimeInauspiciousPeriods: def test_monday_rahu_kala_uses_second_segment(self): result = calc_daytime_inauspicious_periods(1, '06:00', '18:00') assert result['rahu_kala']['segment'] == 2 assert result['rahu_kala']['start'] == '07:30' assert result['rahu_kala']['end'] == '09:00' def test_gulika_and_yamaganda_present(self): result = calc_daytime_inauspicious_periods(4, '06:00', '18:00') assert result['policy'] == 'daytime_8_segments_local_clock' assert 'yamaganda' in result assert 'gulika' in result def test_location_aware_sunrise_sunset(self): result = calc_sunrise_sunset_local(2026, 6, 22, 28.6, 77.2, 5.5) assert result['policy'] in {'swisseph_rise_trans', 'location_aware_solar_event_approximation'} assert ':' in result['sunrise'] assert ':' in result['sunset'] # ── Panchanga Complete Tests ──────────────────────────────────────── class TestPanchangaComplete: def test_five_elements_present(self): result = calc_panchanga(45.0, 120.0, 4, 6.0) assert 'tithi' in result assert 'nakshatra' in result assert 'yoga' in result assert 'karana' in result assert 'vara' in result def test_overall_score_range(self): result = calc_panchanga(45.0, 120.0, 4, 6.0) assert 0 <= result['overall_score'] <= 1 def test_auspicious_count(self): result = calc_panchanga(45.0, 120.0, 4, 6.0) assert 0 <= result['auspicious_count'] <= 6 def test_warnings_list(self): result = calc_panchanga(45.0, 120.0, 4, 6.0) assert isinstance(result['warnings'], list) def test_panchanga_range_report_includes_inauspicious_periods(self): result = panchanga_range_report('2026-06-22', '2026-06-24') assert result['day_count'] == 3 assert result['days'][0]['query_date'] == '2026-06-22' assert result['days'][0]['inauspicious_periods']['rahu_kala']['label'] == 'Rahu Kala' assert result['calculation_policy']['inauspicious_periods'] def test_panchanga_range_report_can_use_location_aware_solar_times(self): result = panchanga_range_report('2026-06-22', '2026-06-22', lat=28.6, lon=77.2, tz=5.5) assert result['location']['lat'] == 28.6 assert result['calculation_policy']['sunrise_sunset'] in { 'SwissEph rise_trans', 'location-aware solar approximation', 'mixed SwissEph rise_trans with approximation fallback', } assert 'solar_times' in result['days'][0] assert 'end_times' in result['days'][0] assert 'vrata_tags' in result['days'][0] assert 'condition_tags' in result['days'][0] assert 'festival_details' in result['days'][0] assert 'search_summary' in result assert 'condition_counts' in result['search_summary'] assert 'choghadiya' in result['days'][0] assert 'hora_windows' in result['days'][0] def test_panchanga_range_report_includes_month_grid_for_single_month(self): result = panchanga_range_report('2026-06-01', '2026-06-30', activity='business', lat=28.6, lon=77.2, tz=5.5) assert result['month_grid'] cells = [cell for week in result['month_grid'] for cell in week if cell] assert len(cells) == 30 assert cells[0]['date'] == '2026-06-01' assert 'best_activities' in cells[0] assert 'vrata_tags' in cells[0] assert 'condition_tags' in cells[0] def test_festival_details_explain_candidate_basis(self): result = panchanga_range_report('2026-06-01', '2026-06-30', lat=28.6, lon=77.2, tz=5.5) details = [detail for day in result['days'] for detail in day.get('festival_details', [])] assert details assert all('basis' in detail for detail in details) assert any('confirmation_note' in detail for detail in details) assert result['search_summary']['query_examples'] def test_panchanga_end_times_are_available_with_swisseph(self): result = calc_panchanga_end_times(2026, 6, 22, 5.5, 11.4) assert result is not None assert result['policy'] == 'swisseph_boundary_bisection' assert 'tithi' in result assert 'ends_at' in result['tithi'] def test_basic_vrata_tags(self): assert classify_vrata_tags({'tithi_num': 11, 'paksha': 'Shukla', 'name': 'Ekadashi'})[0]['key'] == 'ekadashi' assert classify_vrata_tags({'tithi_num': 28, 'paksha': 'Krishna', 'name': 'Trayodashi'})[0]['key'] == 'pradosham' assert classify_vrata_tags({'tithi_num': 30, 'paksha': 'Krishna', 'name': 'Amavasya'})[0]['key'] == 'amavasya' def test_richer_vrata_tags_use_nakshatra_and_vara(self): tags = classify_vrata_tags( {'tithi_num': 13, 'paksha': 'Shukla', 'name': 'Trayodashi'}, {'nakshatra': 'Pushya'}, {'vara': 'Thursday'}, ) keys = {tag['key'] for tag in tags} assert {'pradosham', 'pushya_nakshatra', 'guru_pushya'} <= keys def test_vrata_tags_mark_masa_dependent_festivals_as_candidates(self): tags = classify_vrata_tags({'tithi_num': 3, 'paksha': 'Shukla', 'name': 'Tritiya'}) assert any(tag['key'] == 'akshaya_tritiya_candidate' and tag['type'] == 'festival_candidate' for tag in tags) def test_panchanga_condition_tags_are_searchable(self): report = { 'panchanga': {'warnings': ['avoid']}, 'summary': {'best_activities': ['business'], 'warnings': ['avoid']}, 'vrata_tags': [{'key': 'ekadashi', 'type': 'fasting'}], 'activity_checks': {'business': {'verdict': '大吉(Excellent)'}}, 'choghadiya': {'day': [{'quality': 'auspicious'}], 'night': []}, } keys = {tag['key'] for tag in classify_panchanga_condition_tags(report, activity='business')} assert {'has_vrata', 'spiritual_practice', 'auspicious_activity', 'avoid_new_start', 'good_choghadiya', 'selected_activity_good'} <= keys def test_choghadiya_and_hora_windows(self): choghadiya = calc_choghadiya_windows(1, '06:00', '18:00') assert choghadiya['day'][0]['name'] == 'Amrit' assert len(choghadiya['day']) == 8 assert len(choghadiya['night']) == 8 hora = calc_hora_windows(1, '06:00', '18:00') assert hora['day'][0]['lord'] == 'Moon' assert len(hora['day']) == 12 assert len(hora['night']) == 12 # ── Activity Muhurta Tests ───────────────────────────────────────── class TestActivityMuhurta: def test_marriage_activity(self): panchanga = calc_panchanga(45.0, 120.0, 4, 6.0) result = check_activity_muhurta(panchanga, 'marriage') assert result['verdict'] is not None def test_all_5_activities(self): panchanga = calc_panchanga(45.0, 120.0, 4, 6.0) for act in ['marriage', 'business', 'travel', 'medical', 'education']: result = check_activity_muhurta(panchanga, act) assert 'verdict' in result def test_unknown_activity_error(self): panchanga = calc_panchanga(45.0, 120.0, 4, 6.0) result = check_activity_muhurta(panchanga, 'unknown') assert 'error' in result def test_good_tithi_for_marriage(self): # Create a panchanga with tithi 2 (good for marriage) panchanga = calc_panchanga(0.0, 24.0, 4, 6.0) # tithi 3 result = check_activity_muhurta(panchanga, 'marriage') assert result['tithi_eval'] in ('good', 'neutral', 'bad') def test_pushya_nakshatra_good_for_business(self): # Pushya nakshatra center ≈ 104° panchanga = calc_panchanga(80.0, 104.0, 4, 6.0) result = check_activity_muhurta(panchanga, 'business') if panchanga['nakshatra']['nakshatra'] == 'Pushya': assert result['nakshatra_eval'] == 'good' # ── Full Report Tests ─────────────────────────────────────────────── class TestMuhurtaFullReport: def test_report_structure(self): result = muhurta_full_report(45.0, 120.0, 4, 6.0, '2026-06-04') assert 'panchanga' in result assert 'abhijit_muhurta' in result assert 'activity_checks' in result assert 'summary' in result def test_best_activities_list(self): result = muhurta_full_report(45.0, 120.0, 4, 6.0) assert isinstance(result['summary']['best_activities'], list) def test_avoid_activities_list(self): result = muhurta_full_report(45.0, 120.0, 4, 6.0) assert isinstance(result['summary']['avoid_activities'], list) def test_query_date_preserved(self): result = muhurta_full_report(45.0, 120.0, 4, 6.0, '2026-06-04') assert result['query_date'] == '2026-06-04' class TestMuhurtaRangeSearch: def test_returns_ranked_activity_windows_for_date_range(self): result = muhurta_range_search( '2026-06-22', '2026-06-28', activity='business', limit=3, sunrise='06:00', sunset='18:00', ) assert result['mode'] == 'muhurta_date_range_solver' assert result['activity'] == 'business' assert result['date_range'] == {'start': '2026-06-22', 'end': '2026-06-28'} assert result['candidate_count'] <= 3 assert result['best_windows'] assert result['rejected_dates'] assert result['constraints']['avoid_inauspicious_periods'] is True first = result['best_windows'][0] assert {'date', 'score', 'quality', 'activity_verdict', 'recommended_windows', 'evidence'} <= set(first) assert first['recommended_windows'] assert first['evidence']['panchanga'] class TestMuhurtaSidecar: def test_build_muhurta_sidecar_returns_compact_workflow_shape(self): result = build_muhurta_sidecar( date_str='2026-07-08', activity='business', lat=36.42, lon=114.2, tz=8, ) assert result['status'] == 'ok' assert result['source'] == 'local_muhurta.py' assert result['activity'] == 'business' assert result['report_mode'] == 'muhurta_date_range_solver' assert result['panchanga']['query_date'] == '2026-07-08' assert result['best_windows'] assert 'next_action' in result