"""shinsal.py — 지장간/신살/공망/지지 상호작용 검증.

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.shinsal import (
    get_hidden_stems,
    get_all_hidden_stems,
    analyze_branch_interactions,
    calculate_shinsal,
    calculate_gongmang,
)


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. 지장간 단순 테스트 ────────────────────────────────────────────
def test_hidden_stems_basic():
    assert get_hidden_stems("子") == ["癸"]
    assert get_hidden_stems("丑") == ["己", "癸", "辛"]
    assert get_hidden_stems("寅") == ["甲", "丙", "戊"]
    assert get_hidden_stems("卯") == ["乙"]
    assert get_hidden_stems("辰") == ["戊", "乙", "癸"]
    assert get_hidden_stems("巳") == ["丙", "庚", "戊"]
    assert get_hidden_stems("午") == ["丁", "己"]
    assert get_hidden_stems("未") == ["己", "丁", "乙"]
    assert get_hidden_stems("申") == ["庚", "壬", "戊"]
    assert get_hidden_stems("酉") == ["辛"]
    assert get_hidden_stems("戌") == ["戊", "辛", "丁"]
    assert get_hidden_stems("亥") == ["壬", "甲"]


# ─── 2. hiddenStems reference 매칭 ───────────────────────────────────
@pytest.mark.parametrize(
    "case",
    REF,
    ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_hidden_stems_match_reference(case):
    inp = case["input"]
    expected_raw = case["expected"]["analysis"].get("hiddenStems")
    if expected_raw is None:
        pytest.skip("no hiddenStems in expected")
    expected = _normalize(expected_raw)

    saju = calculate_saju(
        inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
    )
    actual = get_all_hidden_stems(saju)
    assert actual == expected, f"mismatch for {inp}\nactual={actual}\nexpected={expected}"


# ─── 3. branchInteractions reference 매칭 ────────────────────────────
@pytest.mark.parametrize(
    "case",
    REF,
    ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_branch_interactions_match_reference(case):
    inp = case["input"]
    expected_raw = case["expected"]["analysis"].get("branchInteractions")
    if expected_raw is None:
        pytest.skip("no branchInteractions")
    expected = _normalize(expected_raw)

    saju = calculate_saju(
        inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
    )
    actual = analyze_branch_interactions(saju)
    # 순서가 다를 수 있으므로 정렬 후 비교
    assert sorted(map(_canonical, actual)) == sorted(map(_canonical, expected)), (
        f"mismatch for {inp}\nactual={actual}\nexpected={expected}"
    )


# ─── 4. shinsal reference 매칭 ───────────────────────────────────────
@pytest.mark.parametrize(
    "case",
    REF,
    ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_shinsal_match_reference(case):
    inp = case["input"]
    expected_raw = case["expected"]["analysis"].get("shinsal")
    if expected_raw is None:
        pytest.skip("no shinsal")
    expected = _normalize(expected_raw)

    saju = calculate_saju(
        inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
    )
    actual = calculate_shinsal(saju)
    assert sorted(map(_canonical, actual)) == sorted(map(_canonical, expected)), (
        f"mismatch for {inp}\nactual={actual}\nexpected={expected}"
    )


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

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

    # branches / branches_kr / description 전체 비교
    assert actual.get("branches") == expected.get("branches"), (
        f"branches mismatch for {inp}\nactual={actual}\nexpected={expected}"
    )
    assert actual.get("branches_kr") == expected.get("branches_kr"), (
        f"branches_kr mismatch for {inp}\nactual={actual}\nexpected={expected}"
    )
    assert actual.get("description") == expected.get("description"), (
        f"description mismatch for {inp}\nactual={actual}\nexpected={expected}"
    )
