"""compatibility.py — 두 사주 궁합 점수 + breakdown 검증. saju-web/app/compatibility/result/page.tsx의 calculateCompatibility() 로직과 1:1 매핑. 알고리즘: score = 50 (base) + (same: +10 / produce|produced: +25 / overcome|overcomed: -10) + (six_harmony: +20) + (three_harmony: +15) + (conflict: -20) clamp(0, 100) """ from app.calculator.core import calculate_saju from app.calculator.compatibility import ( calculate_compatibility, _get_element_relation, _get_branch_relation, ) # ─── 단위 헬퍼 테스트 ───────────────────────────────────────────── def test_element_relation_same(): assert _get_element_relation("金", "金") == "same" def test_element_relation_produce(): # 木 → 火 (목생화): el1 produces el2 assert _get_element_relation("木", "火") == "produce" def test_element_relation_produced(): # 火 ← 木: el1 is produced by el2 assert _get_element_relation("火", "木") == "produced" def test_element_relation_overcome(): # 金 → 木 (금극목): el1 overcomes el2 assert _get_element_relation("金", "木") == "overcome" def test_element_relation_overcomed(): # 木 ← 金: el1 is overcomed by el2 assert _get_element_relation("木", "金") == "overcomed" def test_branch_six_harmony(): # 寅亥 6합 rel = _get_branch_relation("寅", "亥") assert rel["six_harmony"] is True assert rel["conflict"] is False def test_branch_three_harmony(): # 亥卯未 3합 — 亥+卯 rel = _get_branch_relation("亥", "卯") assert rel["three_harmony"] is True def test_branch_conflict(): # 子午 충 rel = _get_branch_relation("子", "午") assert rel["conflict"] is True assert rel["six_harmony"] is False def test_branch_same_not_three_harmony(): # 같은 지지는 3합 아님 (서로 다른 지지여야 합) rel = _get_branch_relation("未", "未") assert rel["three_harmony"] is False assert rel["six_harmony"] is False assert rel["conflict"] is False # ─── 통합: calculate_compatibility ──────────────────────────────── def test_score_range_0_to_100(): """모든 결과가 0~100 범위 + breakdown 키 존재.""" a = calculate_saju(1990, 5, 15, 14, "male") b = calculate_saju(1992, 8, 8, 18, "female") result = calculate_compatibility(a, b) assert 0 <= result["score"] <= 100 assert "breakdown" in result bd = result["breakdown"] assert "day_master_element" in bd assert "branch_interaction" in bd assert "base" in bd assert bd["base"] == 50 def test_same_saju_same_element_no_branch_interaction(): """1990-5-15 male+female (둘 다 辛未/金) — same element, 같은 일지(중립) → 50+10+0 = 60.""" a = calculate_saju(1990, 5, 15, 14, "male") b = calculate_saju(1990, 5, 15, 14, "female") result = calculate_compatibility(a, b) assert result["score"] == 60 assert result["breakdown"]["day_master_element"]["relation"] == "same" assert result["breakdown"]["day_master_element"]["score"] == 10 assert result["breakdown"]["branch_interaction"]["score"] == 0 def test_six_harmony_plus_produce_high_score(): """1985-1-1 (辛卯/金) + 1989-7-10 (壬戌/水) → 金生水 (produce) +25, 卯戌 6합 +20 → 50+25+20 = 95. """ a = calculate_saju(1985, 1, 1, 12, "male") b = calculate_saju(1989, 7, 10, 12, "male") result = calculate_compatibility(a, b) assert result["score"] == 95 bd = result["breakdown"] assert bd["day_master_element"]["relation"] == "produce" assert bd["day_master_element"]["score"] == 25 assert bd["branch_interaction"]["flags"]["six_harmony"] is True assert bd["branch_interaction"]["score"] == 20 def test_conflict_plus_overcome_low_score(): """1988-12-1 (辛巳/金) + 1992-3-21 (丁亥/火) → 火→金 (overcomed) -10, 巳亥 충 -20 → 50-10-20 = 20. """ a = calculate_saju(1988, 12, 1, 8, "male") b = calculate_saju(1992, 3, 21, 12, "female") result = calculate_compatibility(a, b) assert result["score"] == 20 bd = result["breakdown"] assert bd["day_master_element"]["relation"] in ("overcome", "overcomed") assert bd["day_master_element"]["score"] == -10 assert bd["branch_interaction"]["flags"]["conflict"] is True assert bd["branch_interaction"]["score"] == -20 def test_three_harmony_plus_overcome(): """1990-5-15 (辛未/金) + 1992-3-21 (丁亥/火) → 火→金 (overcomed) -10, 亥未 3합 +15 → 50-10+15 = 55. """ a = calculate_saju(1990, 5, 15, 14, "male") b = calculate_saju(1992, 3, 21, 12, "female") result = calculate_compatibility(a, b) assert result["score"] == 55 bd = result["breakdown"] assert bd["day_master_element"]["score"] == -10 assert bd["branch_interaction"]["flags"]["three_harmony"] is True assert bd["branch_interaction"]["score"] == 15 def test_score_clamp_at_zero(): """극단 케이스: 상극(-10) + 충(-20) = 50-30 = 20. clamp 동작 검증을 위한 sanity.""" a = calculate_saju(1988, 12, 1, 8, "male") b = calculate_saju(1992, 3, 21, 12, "female") result = calculate_compatibility(a, b) assert result["score"] >= 0 assert result["score"] <= 100 def test_breakdown_descriptions_non_empty(): """breakdown의 description 필드가 비어있지 않음.""" a = calculate_saju(1990, 5, 15, 14, "male") b = calculate_saju(1995, 8, 20, 12, "female") result = calculate_compatibility(a, b) bd = result["breakdown"] assert isinstance(bd["day_master_element"]["description"], str) assert len(bd["day_master_element"]["description"]) > 0 assert isinstance(bd["branch_interaction"]["description"], str) assert len(bd["branch_interaction"]["description"]) > 0 def test_symmetry_score_only(): """A vs B와 B vs A의 score는 동일 (saju-web과 동일하게 대칭). note: relation 태그(produce vs produced)는 비대칭일 수 있으나 delta 점수는 동일하므로 최종 score는 항상 같다. """ a = calculate_saju(1985, 1, 1, 12, "male") b = calculate_saju(1989, 7, 10, 12, "male") r_ab = calculate_compatibility(a, b) r_ba = calculate_compatibility(b, a) assert r_ab["score"] == r_ba["score"]