from __future__ import annotations

from datetime import datetime, timezone

import pytest

from app.features.personalized_reco.content_repository.types import ContentProfileDTO
from app.features.personalized_reco.scoring.defaults import get_default_config
from app.features.personalized_reco.scoring.score import score_content
from app.features.personalized_reco.scoring.types import ExternalTerms, ScoreConfig
from app.features.user_profile_scoring.types import ProfileAnswered, UserProfileV1_2, UserStageOneHot


def _u(
    *,
    stage: str = "unknown",
    emotion_score: float | None = None,
    need: dict[str, int] | None = None,
    context: dict[str, int] | None = None,
    profile_confidence: float = 1.0,
) -> UserProfileV1_2:
    now = datetime.now(tz=timezone.utc)
    if stage == "expecting":
        s = UserStageOneHot(expecting=1, parenting=0, unknown=0)
    elif stage == "parenting":
        s = UserStageOneHot(expecting=0, parenting=1, unknown=0)
    else:
        s = UserStageOneHot(unknown=1)

    return UserProfileV1_2(
        profile_generated_at=now,
        profile_confidence=profile_confidence,
        profile_answered=ProfileAnswered(stage=True, emotion=True, context=True, need=True),
        stage=s,
        emotion_score=emotion_score,
        context=context or {},
        need=need or {},
    )


def _c(
    *,
    stage: str = "general",
    emotion_score: float | None = None,
    need_key: str = "emotional_support",
    context_key: str = "family",
    need_value: float = 1.0,
    context_value: float = 1.0,
    personalization_power: float = 1.0,
    review_confidence: float = 0.7,
) -> ContentProfileDTO:
    return ContentProfileDTO(
        content_id=1,
        text="hello",
        stage=stage,  # type: ignore[arg-type]
        emotion_score=emotion_score,
        context_suitability={context_key: context_value},
        need_suitability={need_key: need_value},
        personalization_power=personalization_power,
        risk_flags=[],
        review_confidence=review_confidence,
    )


def test_missing_fields_defaults_are_applied() -> None:
    u = _u(emotion_score=None, need={}, context={})
    c = _c(stage="general", emotion_score=None)
    r = score_content(scene="feed", user_profile=u, content_profile=c)

    assert r.breakdown.S_need == pytest.approx(0.5)
    assert r.breakdown.S_context == pytest.approx(0.5)
    assert r.breakdown.S_emotion == pytest.approx(0.8)
    assert set(r.breakdown.missing_fields) == {"need", "context", "emotion"}


def test_uncertainty_penalty_enabled_for_push_by_default_and_can_be_disabled() -> None:
    u = _u(stage="unknown", emotion_score=0.6, need={"emotional_support": 1}, context={"family": 1}, profile_confidence=0.2)
    c = _c(stage="general", emotion_score=0.6, personalization_power=1.0, review_confidence=0.2)

    r_on = score_content(scene="push", user_profile=u, content_profile=c)
    assert r_on.breakdown.P_uncertainty > 0

    cfg_off = ScoreConfig.model_validate(get_default_config("push").model_dump() | {"enable_uncertainty_penalty": False})
    r_off = score_content(scene="push", user_profile=u, content_profile=c, config=cfg_off)
    assert r_off.breakdown.P_uncertainty == pytest.approx(0.0)
    assert r_off.final_score > r_on.final_score


def test_widget_emotion_soft_penalty_is_applied_outside_range() -> None:
    u = _u(stage="unknown", emotion_score=0.6, need={"emotional_support": 1}, context={"family": 1})

    cfg = get_default_config("widget")
    assert cfg.widget_emotion_soft_range == (0.4, 0.8)
    assert cfg.widget_emotion_penalty_gamma == pytest.approx(0.25)

    c_in = _c(stage="general", emotion_score=0.6, personalization_power=0.0)
    r_in = score_content(scene="widget", user_profile=u, content_profile=c_in, config=cfg)
    assert r_in.breakdown.P_widget_emotion_out_of_range == pytest.approx(0.0)

    c_out = _c(stage="general", emotion_score=0.0, personalization_power=0.0)
    r_out = score_content(scene="widget", user_profile=u, content_profile=c_out, config=cfg)
    assert r_out.breakdown.P_widget_emotion_out_of_range == pytest.approx(0.25)
    assert r_out.final_score < r_in.final_score


def test_pass_false_forces_final_score_zero_but_breakdown_is_present() -> None:
    u = _u(stage="unknown", emotion_score=0.6, need={"emotional_support": 1}, context={"family": 1})
    c = _c(stage="general", emotion_score=0.6, personalization_power=1.0)
    r = score_content(scene="feed", user_profile=u, content_profile=c, pass_filters=False, external_terms=ExternalTerms())

    assert r.final_score == pytest.approx(0.0)
    assert r.breakdown.passed is False
    # breakdown 字段集合稳定（至少包含关键分解项）
    d = r.breakdown.model_dump(by_alias=True)
    for k in [
        "scene",
        "pass",
        "S_need",
        "S_context",
        "S_stage",
        "S_emotion",
        "S_core",
        "S_personal",
        "S_fresh",
        "P_fatigue",
        "P_repeat",
        "P_risk",
        "P_uncertainty",
        "P_widget_emotion_out_of_range",
        "missing_fields",
    ]:
        assert k in d

