from __future__ import annotations

from datetime import datetime, timezone

import pytest

from app.features.personalized_reco.content_repository.interface import ContentRepository
from app.features.personalized_reco.content_repository.types import ContentProfileDTO
from app.features.personalized_reco.reco_engine.orchestrator import recommend
from app.features.personalized_reco.reco_engine.types import RecoConstraints
from app.features.user_profile_scoring.types import ProfileAnswered, UserProfileV1_2, UserStageOneHot


class _FakeRepo(ContentRepository):
    def __init__(self, candidates_by_level: dict[int, list[ContentProfileDTO]]):
        self._candidates_by_level = candidates_by_level

    async def fetch_candidates(  # type: ignore[override]
        self,
        *,
        scene: str,
        user_profile: object,
        fallback_level: int,
        limit: int,
        locale: str,
        exclude_content_ids: list[int] | None = None,
    ) -> list[ContentProfileDTO]:
        # 单测：简化实现，只按 level 返回，忽略 limit/locale/exclude
        return list(self._candidates_by_level.get(int(fallback_level), []))[: int(limit)]

    async def fetch_contents_by_ids(self, *, content_ids: list[int], locale: str) -> list[ContentProfileDTO]:  # type: ignore[override]
        raise NotImplementedError


def _now() -> datetime:
    return datetime(2026, 2, 2, 12, 0, 0, tzinfo=timezone.utc)


def _user_profile(*, stage: str = "unknown", emotion_score: float | None = 0.5) -> UserProfileV1_2:
    if stage == "expecting":
        st = UserStageOneHot(expecting=1, parenting=0, unknown=0)  # type: ignore[arg-type]
    elif stage == "parenting":
        st = UserStageOneHot(expecting=0, parenting=1, unknown=0)  # type: ignore[arg-type]
    else:
        st = UserStageOneHot(expecting=0, parenting=0, unknown=1)  # type: ignore[arg-type]

    return UserProfileV1_2(
        profile_generated_at=_now(),
        profile_confidence=1.0,
        profile_answered=ProfileAnswered(stage=True, emotion=emotion_score is not None, context=False, need=False),
        stage=st,
        emotion_score=emotion_score,
        context={},
        need={},
    )


def _content(
    *,
    content_id: int,
    text: str = "hello",
    stage: str = "general",
    personalization_power: float = 0.0,
    risk_flags: list[str] | None = None,
    author_id: str | None = None,
    template_id: str | None = None,
    need_suitability: dict[str, float] | None = None,
) -> ContentProfileDTO:
    return ContentProfileDTO(
        content_id=int(content_id),
        text=text,
        stage=stage,  # type: ignore[arg-type]
        emotion_score=None,
        context_suitability={},
        need_suitability=need_suitability or {},
        personalization_power=float(personalization_power),
        risk_flags=risk_flags or [],
        author_id=author_id,
        template_id=template_id,
        review_confidence=0.7,
    )


@pytest.mark.asyncio
async def test_reco_engine_k_zero_returns_empty() -> None:
    repo = _FakeRepo({0: [_content(content_id=1)]})
    res = await recommend(
        repo=repo,
        scene="feed",
        user_profile=_user_profile(),
        already_recommended_ids=[],
        touched_or_viewed_ids=[],
        k=0,
        now=_now(),
        locale=None,
    )
    assert res.items == []
    assert res.meta.served_k == 0


@pytest.mark.asyncio
async def test_reco_engine_pool_empty_sets_empty_reason() -> None:
    repo = _FakeRepo({0: []})
    res = await recommend(
        repo=repo,
        scene="feed",
        user_profile=_user_profile(),
        already_recommended_ids=[],
        touched_or_viewed_ids=[],
        k=1,
        now=_now(),
        locale="en",
    )
    assert res.items == []
    assert res.meta.empty_reason == "pool_empty"


@pytest.mark.asyncio
async def test_reco_engine_hard_filter_all_sets_empty_reason() -> None:
    repo = _FakeRepo({0: [_content(content_id=1, risk_flags=["block_health_medical"])]})
    res = await recommend(
        repo=repo,
        scene="push",
        user_profile=_user_profile(stage="unknown"),
        already_recommended_ids=[],
        touched_or_viewed_ids=[],
        k=1,
        now=_now(),
        locale="en",
    )
    assert res.items == []
    assert res.meta.empty_reason == "hard_filter_all"
    assert res.meta.risk_filtered_count_by_flag.get("block_health_medical", 0) == 1


@pytest.mark.asyncio
async def test_reco_engine_freqcap_all_sets_empty_reason() -> None:
    # Push 场景：作者冷却命中，导致 after_freqcap=0
    repo = _FakeRepo({0: [_content(content_id=1, author_id="a1")]})
    res = await recommend(
        repo=repo,
        scene="push",
        user_profile=_user_profile(stage="unknown"),
        already_recommended_ids=[],
        touched_or_viewed_ids=[],
        k=1,
        now=_now(),
        locale="en",
        constraints=RecoConstraints(recent_author_ids=["a1"]),
    )
    assert res.items == []
    assert res.meta.empty_reason == "freqcap_all"


@pytest.mark.asyncio
async def test_reco_engine_returns_item_and_explanations_enabled() -> None:
    repo = _FakeRepo({0: [_content(content_id=1, text="t1", personalization_power=0.0)]})
    res = await recommend(
        repo=repo,
        scene="feed",
        user_profile=_user_profile(stage="unknown"),
        already_recommended_ids=[],
        touched_or_viewed_ids=[],
        k=1,
        now=_now(),
        locale="en",
    )
    assert len(res.items) == 1
    assert res.items[0].content_id == 1
    assert res.items[0].text == "t1"
    assert res.items[0].explanations is not None


