"""analysis.py — 오행 점수/신강신약/용신/세운/perform_full_analysis 검증.

fixtures/reference_saju.json 의 30 case (TS) 와 Python 구현 1:1 일치 검증.
"""
import json
from pathlib import Path

import pytest

from app.calculator.core import calculate_saju
from app.calculator.analysis import (
    calculate_detailed_element_balance,
    calculate_element_score,
    analyze_day_master_strength,
    estimate_yongshin,
    calculate_seun,
    perform_full_analysis,
)


REF_PATH = Path(__file__).parent / "fixtures" / "reference_saju.json"
REF = json.loads(REF_PATH.read_text(encoding="utf-8"))


def _camel_to_snake(name: str) -> str:
    out = []
    for ch in name:
        if ch.isupper():
            out.append("_" + ch.lower())
        else:
            out.append(ch)
    return "".join(out)


def _normalize(d):
    """TS camelCase → Python snake_case (deep). 값은 변경하지 않음."""
    if isinstance(d, dict):
        return {_camel_to_snake(k): _normalize(v) for k, v in d.items()}
    if isinstance(d, list):
        return [_normalize(x) for x in d]
    return d


def _canonical(item):
    """dict/list 비교용 — 안정 정렬을 위한 JSON 직렬화."""
    return json.dumps(item, ensure_ascii=False, sort_keys=True)


# ─── 1. element_balance reference 매칭 ───────────────────────────────
@pytest.mark.parametrize(
    "case",
    REF,
    ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_element_balance_match_reference(case):
    inp = case["input"]
    expected = case["expected"]["analysis"]["elementBalance"]

    saju = calculate_saju(inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"])
    actual = calculate_detailed_element_balance(saju)

    for elem in ["木", "火", "土", "金", "水"]:
        assert abs(actual[elem] - expected[elem]) < 0.01, (
            f"{inp}: element_balance.{elem} differ: {actual[elem]} vs {expected[elem]}"
        )


# ─── 2. element_scores reference 매칭 ────────────────────────────────
@pytest.mark.parametrize(
    "case",
    REF,
    ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_element_score_match_reference(case):
    inp = case["input"]
    expected = case["expected"]["analysis"]["elementScores"]

    saju = calculate_saju(inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"])
    actual = calculate_element_score(saju)

    for elem, expected_pct in expected.items():
        actual_pct = actual.get(elem, 0)
        assert actual_pct == expected_pct, (
            f"{inp}: element_scores.{elem}: {actual_pct} vs {expected_pct}"
        )


# ─── 3. day_master_strength reference 매칭 ───────────────────────────
@pytest.mark.parametrize(
    "case",
    REF,
    ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_day_master_strength_match_reference(case):
    inp = case["input"]
    expected = case["expected"]["analysis"]["dayMasterStrength"]

    saju = calculate_saju(inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"])
    actual = analyze_day_master_strength(saju)

    assert actual["result"] == expected["result"], (
        f"{inp}: result mismatch: {actual['result']} vs {expected['result']}"
    )
    assert actual["score"] == expected["score"], (
        f"{inp}: score mismatch: {actual['score']} vs {expected['score']}"
    )
    assert actual["reasons"] == expected["reasons"], (
        f"{inp}: reasons mismatch:\n  actual={actual['reasons']}\n  expected={expected['reasons']}"
    )


# ─── 4. yong_shin reference 매칭 ─────────────────────────────────────
@pytest.mark.parametrize(
    "case",
    REF,
    ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_yong_shin_match_reference(case):
    inp = case["input"]
    expected_raw = case["expected"]["analysis"]["yongShin"]
    expected = _normalize(expected_raw)

    saju = calculate_saju(inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"])
    strength = analyze_day_master_strength(saju)
    actual = estimate_yongshin(saju, strength)

    assert actual["yong_shin"] == expected["yong_shin"]
    assert actual["yong_shin_kr"] == expected["yong_shin_kr"]
    assert actual["hee_shin"] == expected["hee_shin"]
    assert actual["hee_shin_kr"] == expected["hee_shin_kr"]
    assert actual["gi_shin"] == expected["gi_shin"]
    assert actual["gi_shin_kr"] == expected["gi_shin_kr"]
    assert actual["explanation"] == expected["explanation"]


# ─── 5. seun reference 매칭 ─────────────────────────────────────────
@pytest.mark.parametrize(
    "case",
    REF,
    ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_seun_match_reference(case):
    inp = case["input"]
    expected_raw = case["expected"]["analysis"]["seun"]
    expected = _normalize(expected_raw)

    saju = calculate_saju(inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"])
    actual = calculate_seun(2026, saju)

    assert actual["stem"] == expected["stem"]
    assert actual["branch"] == expected["branch"]
    assert actual["stem_kr"] == expected["stem_kr"]
    assert actual["branch_kr"] == expected["branch_kr"]
    assert actual["element"] == expected["element"]
    assert actual["element_kr"] == expected["element_kr"]
    assert actual["year"] == expected["year"]
    # interactions — 순서 무관 비교
    actual_iter = actual.get("interactions", [])
    expected_iter = expected.get("interactions", [])
    assert sorted(map(_canonical, actual_iter)) == sorted(map(_canonical, expected_iter)), (
        f"{inp}: seun interactions mismatch:\n  actual={actual_iter}\n  expected={expected_iter}"
    )


# ─── 6. perform_full_analysis 통합 ───────────────────────────────────
@pytest.mark.parametrize(
    "case",
    REF,
    ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_perform_full_analysis_keys(case):
    """perform_full_analysis 가 모든 expected 키를 반환하는지 확인."""
    inp = case["input"]
    saju = calculate_saju(inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"])
    actual = perform_full_analysis(saju, 2026)

    # 통합 결과에 필수 키 모두 존재
    for key in [
        "element_balance", "element_scores", "day_master_strength", "yong_shin",
        "branch_interactions", "shinsal", "gongmang", "seun", "hidden_stems",
    ]:
        assert key in actual, f"{inp}: missing key {key}"
