from __future__ import annotations

import asyncio
import itertools
from types import SimpleNamespace
from unittest.mock import AsyncMock

import pytest
from checkin_cli.customer_admin import CustomerAdminError

from gateway.config import PlatformConfig
from gateway.platforms.telegram import TelegramAdapter
from gateway.platforms.nutrition_onboarding_reconciliation import (
    NutritionOnboardingClarification,
    NutritionOnboardingReconciliation,
)
from gateway.platforms.telegram_nutrition_onboarding_preview import (
    PreviewRoute,
    TelegramNutritionOnboardingPreview,
)


OWNER_ID = 8_693_203_710
PREVIEW_CHAT_ID = -1_004_290_459_350
TOPIC_ID = 777
VALID_ANSWERS = (
    "1990-01-01",
    "남성",
    "180",
    "80",
    "보통",
    "주 3회 운동",
    "감량",
    "75",
    "2026-12-01",
    "없음",
    "없음",
    "없음",
    "없음",
    "균형식",
    "없음",
    "없음",
    "아니오",
    "아니오",
    "가능",
    "보통",
    "3",
    "없음",
)


def _message(
    text: str,
    *,
    prompt_id: int | None = None,
    message_id: int = 900,
) -> SimpleNamespace:
    return SimpleNamespace(
        text=text,
        message_id=message_id,
        message_thread_id=TOPIC_ID,
        is_topic_message=True,
        from_user=SimpleNamespace(id=OWNER_ID),
        chat=SimpleNamespace(id=PREVIEW_CHAT_ID, type="supergroup"),
        reply_to_message=(
            SimpleNamespace(message_id=prompt_id)
            if prompt_id is not None
            else None
        ),
    )


def _preview(
    sender: AsyncMock,
) -> TelegramNutritionOnboardingPreview:
    route = PreviewRoute(
        str(OWNER_ID),
        str(PREVIEW_CHAT_ID),
        str(TOPIC_ID),
    )
    return TelegramNutritionOnboardingPreview(lambda: route, sender)


def _sender() -> AsyncMock:
    receipts = itertools.count(901)

    async def send(
        _route: PreviewRoute,
        _text: str,
        _force_reply: bool,
        _reply_to_message_id: str | None,
        _action: str | None = None,
    ) -> SimpleNamespace:
        return SimpleNamespace(message_id=next(receipts))

    return AsyncMock(side_effect=send)


def _adapter_with_dm_topic(
    *,
    chat_id: object,
    thread_id: object | None = None,
) -> TelegramAdapter:
    topic = {"name": "[PREVIEW] 영양 온보딩"}
    if thread_id is not None:
        topic["thread_id"] = thread_id
    return TelegramAdapter(
        PlatformConfig(
            enabled=True,
            token="test-token",
            extra={
                "adaptive_nutrition": {
                    "enabled": True,
                    "review_operator": {
                        "user_id": "owner",
                        "chat_id": "review-chat",
                        "topic_id": 59,
                        "version": 1,
                    },
                },
                "dm_topics": [
                    {
                        "chat_id": chat_id,
                        "topics": [topic],
                    }
                ],
            },
        )
    )


def test_unassigned_disjoint_dm_topic_does_not_break_startup_validation() -> None:
    adapter = _adapter_with_dm_topic(chat_id=OWNER_ID)

    adapter._validate_adaptive_review_space_config()


def test_persisted_dm_topic_collision_still_fails_closed() -> None:
    adapter = _adapter_with_dm_topic(
        chat_id="review-chat",
        thread_id=59,
    )

    with pytest.raises(
        CustomerAdminError,
        match="review space collides with generic reserved route",
    ):
        adapter._validate_adaptive_review_space_config()


@pytest.mark.asyncio
async def test_concurrent_duplicate_reply_advances_exactly_once() -> None:
    sender = _sender()
    preview = _preview(sender)
    await preview.handle_command(_message("/nutritionpreview"))
    reply = _message("1990-01-01", prompt_id=901)

    await asyncio.gather(
        preview.handle_text(reply),
        preview.handle_text(reply),
    )

    assert sender.await_count == 2


@pytest.mark.asyncio
async def test_unanchored_group_answer_is_consumed_without_advancing() -> None:
    sender = _sender()
    preview = _preview(sender)
    await preview.handle_command(_message("/nutritionpreview"))
    answer = _message("1990-01-01", message_id=1000)

    await asyncio.gather(
        preview.handle_text(answer),
        preview.handle_text(answer),
    )

    assert sender.await_count == 1


@pytest.mark.asyncio
async def test_ambiguity_follow_up_precedes_confirmable_summary() -> None:
    sender = _sender()
    preview = _preview(sender)
    clarification = NutritionOnboardingReconciliation(
        summary_ko="식사 횟수를 확인해야 합니다.",
        facts_ko=(),
        ambiguities_ko=("식사 횟수가 모호합니다.",),
        contradictions_ko=(),
        safety_observations_ko=(),
        clarifications=(
            NutritionOnboardingClarification(
                field="meal_count",
                kind="ambiguity",
                question_ko=(
                    "간식은 제외하고 실제 하루 식사 횟수를 알려주세요."
                ),
            ),
        ),
    )
    final = NutritionOnboardingReconciliation(
        summary_ko="하루 네 끼 입력을 확인했습니다.",
        facts_ko=("하루 네 끼를 희망합니다.",),
        ambiguities_ko=(),
        contradictions_ko=(),
        safety_observations_ko=(),
    )
    preview._reconciler = SimpleNamespace(
        reconcile=AsyncMock(side_effect=(clarification, final))
    )
    await preview.handle_command(_message("/nutritionpreview"))
    prompt_id = 901
    for index, answer in enumerate(VALID_ANSWERS):
        await preview.handle_text(
            _message(
                answer,
                prompt_id=prompt_id,
                message_id=1000 + index,
            )
        )
        prompt_id = 902 + index

    assert preview._session is not None
    assert "간식은 제외하고" in sender.await_args.args[1]
    assert sender.await_args.args[2] is True
    await preview.handle_text(
        _message("4끼", prompt_id=923, message_id=1100)
    )

    assert preview._reconciler.reconcile.await_count == 2
    assert preview._session is not None
    assert preview._session.answers == ()
    assert "입력 내용을 정리했어요" in sender.await_args.args[1]
    assert sender.await_args.args[4] == "confirm"


@pytest.mark.asyncio
async def test_owner_qa_command_skips_collection_to_one_clarification() -> None:
    sender = _sender()
    preview = _preview(sender)
    result = NutritionOnboardingReconciliation(
        summary_ko="일정 제약을 확인해야 합니다.",
        facts_ko=(),
        ambiguities_ko=("식사 가능 시간이 모호합니다.",),
        contradictions_ko=(),
        safety_observations_ko=(),
        clarifications=(
            NutritionOnboardingClarification(
                field="schedule_constraints",
                kind="ambiguity",
                question_ko="평일 식사가 가능한 시간을 알려주세요.",
            ),
        ),
    )
    preview._reconciler = SimpleNamespace(
        reconcile=AsyncMock(return_value=result)
    )

    assert await preview.handle_command(
        _message("/nutritionpreview qa")
    )

    assert preview._session is not None
    assert preview._session.phase == "clarifying"
    assert (
        preview._session.clarifications[0].field
        == "schedule_constraints"
    )
    assert "평일 식사가 가능한 시간" in sender.await_args.args[1]
    assert sender.await_args.args[2] is True
    answers = preview._reconciler.reconcile.await_args.args[0]
    assert len(answers) == 22
    assert answers["schedule_constraints"] == (
        "아직 잘 모르겠고 상황에 따라 달라요"
    )


@pytest.mark.asyncio
async def test_exact_preview_confirm_callback_advances_once() -> None:
    sender = _sender()
    preview = _preview(sender)
    final = NutritionOnboardingReconciliation(
        summary_ko="입력 내용을 확인했습니다.",
        facts_ko=("하루 세 끼를 희망합니다.",),
        ambiguities_ko=(),
        contradictions_ko=(),
        safety_observations_ko=(),
    )
    preview._reconciler = SimpleNamespace(
        reconcile=AsyncMock(return_value=final)
    )
    await preview.handle_command(_message("/nutritionpreview"))
    prompt_id = 901
    for index, answer in enumerate(VALID_ANSWERS):
        await preview.handle_text(
            _message(
                answer,
                prompt_id=prompt_id,
                message_id=1200 + index,
            )
        )
        prompt_id = 902 + index
    assert preview._session is not None
    summary_id = int(preview._session.expected_message_id)
    answer = AsyncMock()
    retire = AsyncMock()
    query = SimpleNamespace(
        data="npv:confirm",
        from_user=SimpleNamespace(id=OWNER_ID),
        message=_message("", message_id=summary_id),
        answer=answer,
        edit_message_reply_markup=retire,
    )

    assert await preview.handle_callback(query) is True
    assert preview._session is None
    assert "운영자 검토" in sender.await_args.args[1]
    answer.assert_awaited_once()
    retire.assert_awaited_once_with(reply_markup=None)
    sends_after_first_click = sender.await_count
    assert await preview.handle_callback(query) is True
    assert sender.await_count == sends_after_first_click


@pytest.mark.asyncio
async def test_final_question_purges_before_completion_publication() -> None:
    sender = _sender()
    preview = _preview(sender)
    reconciliation = NutritionOnboardingReconciliation(
        summary_ko="하루 세 끼와 감량 목표를 확인했습니다.",
        facts_ko=("알레르기는 없습니다.",),
        ambiguities_ko=(),
        contradictions_ko=(),
        safety_observations_ko=(),
    )
    preview._reconciler = SimpleNamespace(
        reconcile=AsyncMock(return_value=reconciliation)
    )
    send = sender.side_effect

    async def assert_purged_before_summary(
        route: PreviewRoute,
        text: str,
        force_reply: bool,
        reply_to_message_id: str | None,
        action: str | None = None,
    ) -> SimpleNamespace:
        if reconciliation.summary_ko in text:
            assert preview._session is None
            assert action == "confirm"
        return await send(
            route,
            text,
            force_reply,
            reply_to_message_id,
            action,
        )

    sender.side_effect = assert_purged_before_summary
    await preview.handle_command(_message("/nutritionpreview"))
    prompt_id = 901
    final_reply = None
    for index, answer in enumerate(VALID_ANSWERS):
        final_reply = _message(
            answer,
            prompt_id=prompt_id,
            message_id=1000 + index,
        )
        await preview.handle_text(final_reply)
        prompt_id = 902 + index
    assert final_reply is not None

    await preview.handle_text(final_reply)

    reconciled = preview._reconciler.reconcile.await_args.args[0]
    assert len(reconciled) == 22
    assert reconciled["meal_count"] == 3
    assert sender.await_count == 23
    assert reconciliation.summary_ko in (
        sender.await_args.args[1]
    )


@pytest.mark.asyncio
async def test_stop_and_provider_failure_leave_no_active_preview() -> None:
    sender = _sender()
    preview = _preview(sender)
    await preview.handle_command(_message("/nutritionpreview"))
    await preview.handle_command(_message("/nutritionpreview stop"))
    await preview.handle_text(_message("1990-01-01", prompt_id=901))
    assert sender.await_count == 2

    failed_sender = AsyncMock(side_effect=RuntimeError("provider unavailable"))
    failed = _preview(failed_sender)
    await failed.handle_command(_message("/nutritionpreview"))
    await failed.handle_text(_message("1990-01-01", prompt_id=901))
    assert failed_sender.await_count == 1
