from __future__ import annotations

from datetime import datetime, timezone

import pytest

from app.features.personalized_reco.observability.builder import RecoMetaBuilder
from app.features.personalized_reco.observability.utils import compute_empty_reason, compute_missing_fields
from app.features.user_profile_scoring.types import ProfileAnswered, UserProfileV1_2, UserStageOneHot


def _u(*, need: dict | None = None, context: dict | None = None, emotion_score=None, conf_u: float = 0.9) -> UserProfileV1_2:
    now = datetime.now(tz=timezone.utc)
    return UserProfileV1_2(
        profile_generated_at=now,
        profile_confidence=conf_u,
        profile_answered=ProfileAnswered(stage=True, emotion=True, context=True, need=True),
        stage=UserStageOneHot(unknown=1),
        emotion_score=emotion_score,
        context=context or {},
        need=need or {},
    )


def test_compute_missing_fields() -> None:
    u1 = _u(need={}, context={}, emotion_score=None)
    m1 = compute_missing_fields(u1)
    assert m1.need is True
    assert m1.context is True
    assert m1.emotion is True

    u2 = _u(need={"x": 1}, context={"y": 1}, emotion_score=0.6)
    m2 = compute_missing_fields(u2)
    assert m2.need is False
    assert m2.context is False
    assert m2.emotion is False


def test_compute_empty_reason_branches() -> None:
    assert (
        compute_empty_reason(
            served_k=1,
            candidate_pool_size_raw=0,
            candidate_pool_size_after_hard_filter=0,
            candidate_pool_size_after_freqcap=0,
        )
        is None
    )

    assert (
        compute_empty_reason(
            served_k=0,
            candidate_pool_size_raw=0,
            candidate_pool_size_after_hard_filter=0,
            candidate_pool_size_after_freqcap=0,
        )
        == "pool_empty"
    )

    assert (
        compute_empty_reason(
            served_k=0,
            candidate_pool_size_raw=10,
            candidate_pool_size_after_hard_filter=0,
            candidate_pool_size_after_freqcap=0,
        )
        == "hard_filter_all"
    )

    assert (
        compute_empty_reason(
            served_k=0,
            candidate_pool_size_raw=10,
            candidate_pool_size_after_hard_filter=5,
            candidate_pool_size_after_freqcap=0,
        )
        == "freqcap_all"
    )

    assert (
        compute_empty_reason(
            served_k=0,
            candidate_pool_size_raw=10,
            candidate_pool_size_after_hard_filter=5,
            candidate_pool_size_after_freqcap=3,
        )
        == "unknown"
    )


def test_builder_outputs_stable_fields_and_monotonic_counts() -> None:
    u = _u(need={"emotional_support": 1}, context={}, emotion_score=None, conf_u=0.2)

    # 故意设置“非单调”的输入，验证 builder 的防御修正
    meta = (
        RecoMetaBuilder(scene="feed", user_profile=u, k=30)
        .set_candidate_pool_size_raw(10)
        .set_after_hard_filter(12)  # 非法：大于 raw
        .set_after_dedup(20)  # 非法：大于 after_hard
        .set_after_freqcap(15)  # 非法：大于 after_dedup（修正后会与 after_dedup 对齐）
        .set_served_k(99)  # 非法：大于 after_freqcap
        .set_fallback_level_final(1, reason="freqcap_all")
        .build()
    )

    d = meta.model_dump()
    for k in [
        "scene",
        "candidate_pool_size_raw",
        "candidate_pool_size_after_hard_filter",
        "candidate_pool_size_after_dedup",
        "candidate_pool_size_after_freqcap",
        "fallback_level_final",
        "served_k",
        "empty_reason",
        "conf_U",
        "missing_fields",
    ]:
        assert k in d

    assert meta.candidate_pool_size_raw == 10
    assert meta.candidate_pool_size_after_hard_filter == 10
    assert meta.candidate_pool_size_after_dedup == 10
    assert meta.candidate_pool_size_after_freqcap == 10
    assert meta.served_k == 10
    assert meta.conf_U == pytest.approx(0.2)
    assert meta.missing_fields.context is True
    assert meta.missing_fields.emotion is True

