from __future__ import annotations

import asyncio
import sys
from pathlib import Path
from types import SimpleNamespace
from typing import override

import pytest


ROOT = Path(__file__).resolve().parents[2]
PROFILE_PACKAGE = ROOT / "dualcoach" / "profile"
NOW = 4_000_000_000
for path in (ROOT, PROFILE_PACKAGE):
    if str(path) not in sys.path:
        sys.path.insert(0, str(path))

from checkin_cli.wizard import WizardService
from gateway.platforms.physique_checkin import (
    CallbackData,
    PhysiqueCheckinBridge,
    WizardReply,
)
from gateway.platforms.physique_checkin_config import PhysiqueCheckinConfig
from gateway.platforms.nutrition_onboarding_reconciliation import (
    NutritionOnboardingClarification,
    unknown_answer_fields,
)
from gateway.platforms.nutrition_onboarding_reconciliation_copy import (
    render_clarification_text,
)
from gateway.platforms.telegram import TelegramAdapter
from gateway.platforms.telegram_nutrition_onboarding_copy import PROMPTS, parse_answer
from gateway.platforms.telegram_nutrition_onboarding_customer_v2 import (
    customer_v2_clarification,
)
from gateway.platforms.telegram_nutrition_onboarding_runtime_publication import (
    TelegramNutritionOnboardingRuntimePublicationMixin,
)


@pytest.mark.parametrize(
    ("field", "answer", "expected"),
    (
        ("date_of_birth", "1990. 3. 2.", "1990-03-02"),
        ("equation_sex_basis", "남자", "male"),
        ("equation_sex_basis", "1", "male"),
        ("activity_category", "3", "moderate"),
        ("goal_type", "살 빼기", "loss"),
        ("allergies", "없어요", {"status": "none", "items": []}),
        ("pregnancy_breastfeeding", "2", False),
        ("target_weight_kg", "아직 없어요", None),
        ("meal_count", "보통 하루 세끼 먹어요", 3),
    ),
)
def test_onboarding_accepts_customer_friendly_answers(
    field: str,
    answer: str,
    expected: object,
) -> None:
    assert parse_answer(field, answer) == expected


@pytest.mark.parametrize(
    ("field", "answer"),
    (
        ("date_of_birth", "생일은 봄이에요"),
        ("equation_sex_basis", "남자 같아요"),
        ("activity_category", "적당히 움직여요"),
        ("goal_type", "건강해지고 싶어요"),
        ("pregnancy_breastfeeding", "아마 아닐 거예요"),
    ),
)
def test_onboarding_rejects_unclear_closed_answers_immediately(
    field: str,
    answer: str,
) -> None:
    with pytest.raises(ValueError):
        _ = parse_answer(field, answer)


def test_customer_v2_parses_plain_age_and_grouped_answers() -> None:
    assert parse_answer(
        "date_of_birth",
        "만 34세",
        questionnaire_version="customer_v2",
    ) == 34
    assert parse_answer(
        "activity_profile",
        "3, 주 3회 헬스하고 하루 8천 보 걸어요",
        questionnaire_version="customer_v2",
    ) == {
        "activity_category": "moderate",
        "activity_rationale": "주 3회 헬스하고 하루 8천 보 걸어요",
    }
    assert parse_answer(
        "food_preferences_profile",
        "1 없음 / 2 고수 / 3 한식",
        questionnaire_version="customer_v2",
    ) == {
        "religious_ethical_exclusions": {"status": "none", "items": []},
        "disliked_foods": {"status": "provided", "items": ["고수"]},
        "dietary_preferences": {"status": "provided", "items": ["한식"]},
    }
    assert parse_answer(
        "lifestyle_profile",
        "1 전자레인지 사용 / 2 한 끼 1만 원 / 3 평일 점심 외식",
        questionnaire_version="customer_v2",
    ) == {
        "cooking_access": "전자레인지 사용",
        "budget_band": "한 끼 1만 원",
        "schedule_constraints": "평일 점심 외식",
    } 


def test_customer_v2_unknown_answers_are_typed_by_field_risk() -> None:
    assert parse_answer(
        "allergies",
        "잘 모르겠어요",
        questionnaire_version="customer_v2",
    ) == {"status": "unknown", "items": []}
    assert (
        parse_answer(
            "pregnancy_breastfeeding",
            "기억이 안 나요",
            questionnaire_version="customer_v2",
        )
        is None
    )
    with pytest.raises(ValueError, match="required"):
        _ = parse_answer(
            "height_cm",
            "모르겠어요.",
            questionnaire_version="customer_v2",
        )


def test_unknown_answer_fields_are_machine_readable() -> None:
    assert unknown_answer_fields(
        {
            "allergies": {"status": "unknown", "items": []},
            "intolerances": {"status": "none", "items": []},
            "pregnancy_breastfeeding": None,
            "eating_disorder_risk": False,
        }
    ) == ("allergies", "pregnancy_breastfeeding")


def test_customer_v2_rejects_duplicate_group_numbers() -> None:
    with pytest.raises(ValueError, match="grouped"):
        _ = parse_answer(
            "food_preferences_profile",
            "1 땅콩 / 1 없음 / 2 없음 / 3 한식",
            questionnaire_version="customer_v2",
        )


def test_customer_v2_copy_is_plain_and_explains_sensitive_questions() -> None:
    assert "생년월일" not in PROMPTS["customer_v2:date_of_birth"]
    assert "만 나이" in PROMPTS["customer_v2:date_of_birth"]
    assert "안전을 확인" in PROMPTS["customer_v2:conditions"]
    assert "진단을 위한 질문은 아니에요" in PROMPTS[
        "customer_v2:eating_disorder_risk"
    ]
    assert "정하지 않았다면 '없음'" not in PROMPTS[
        "customer_v2:target_weight_kg"
    ]
    assert "알레르기나 속이 불편한 음식은 다음 질문" in PROMPTS[
        "customer_v2:food_preferences_profile"
    ]
    assert "반드시 필요한 기준" in PROMPTS[
        "customer_v2:equation_sex_basis"
    ]
    assert "만 나이" in customer_v2_clarification(
        "date_of_birth",
        "legacy",
    )
    assert "1 남성 기준 · 2 여성 기준" in customer_v2_clarification(
        "equation_sex_basis",
        "legacy",
    )


def test_customer_v2_clarification_omits_legacy_format_appendix() -> None:
    text = render_clarification_text(
        NutritionOnboardingClarification(
            field="pregnancy_breastfeeding",
            kind="ambiguity",
            question_ko=customer_v2_clarification(
                "pregnancy_breastfeeding",
                "legacy",
            ),
        ),
        index=1,
        total=1,
        questionnaire_version="customer_v2",
    )

    assert "답변 형식:" not in text
    assert "확실하지 않음" in text
    assert "진단" in text


class _CapturedPublication(TelegramNutritionOnboardingRuntimePublicationMixin):
    def __init__(self) -> None:
        self.actions: tuple[str, ...] | None = None
        self.force_reply: bool | None = None
        self.text: str | None = None

    @override
    async def _send_publication(
        self,
        *,
        session: object,
        service: object,
        status: object,
        text: str,
        role: str,
        payload: dict[str, object],
        actions: list[tuple[str, str]],
        force_reply: bool,
        reply_anchor_message_id: int | None = None,
    ) -> None:
        del (
            session,
            service,
            status,
            role,
            payload,
            reply_anchor_message_id,
        )
        self.text = text
        self.actions = tuple(action for action, _ in actions)
        self.force_reply = force_reply

    async def publish(self, status: object) -> None:
        await self._publish(
            SimpleNamespace(),
            SimpleNamespace(),
            status,
        )


@pytest.mark.parametrize(
    ("answer_count", "next_field", "expected_actions"),
    (
        (0, "date_of_birth", ("later",)),
        (
            5,
            "activity_rationale",
            ("prev", "skip", "later"),
        ),
    ),
)
def test_onboarding_questions_expose_navigation_controls(
    answer_count: int,
    next_field: str,
    expected_actions: tuple[str, ...],
) -> None:
    publication = _CapturedPublication()
    status = SimpleNamespace(
        state=SimpleNamespace(value="collecting"),
        answer_count=answer_count,
        next_field=next_field,
        questionnaire_version=None,
        question_number=answer_count + 1,
        question_total=22,
    )

    asyncio.run(publication.publish(status))

    assert publication.actions == expected_actions
    assert publication.force_reply is False


def test_customer_v2_publication_uses_visible_screen_progress() -> None:
    publication = _CapturedPublication()
    status = SimpleNamespace(
        state=SimpleNamespace(value="collecting"),
        answer_count=1,
        next_field="meal_count",
        questionnaire_version="customer_v2",
        question_number=2,
        question_total=15,
    )

    asyncio.run(publication.publish(status))

    assert publication.text is not None
    assert publication.text.startswith("[2/15]")
    assert "하루에 보통 몇 번" in publication.text
    assert publication.actions == ("prev", "unknown", "later")


def test_input_help_publication_routes_only_owner_resume_action() -> None:
    publication = _CapturedPublication()
    status = SimpleNamespace(
        state=SimpleNamespace(value="input_help"),
        answer_count=1,
        next_field=None,
        questionnaire_version="customer_v2",
        question_number=2,
        question_total=15,
        help_field="meal_count",
        answer_failure_count=2,
    )

    asyncio.run(publication.publish(status))

    assert publication.actions == ("help_resume",)
    assert publication.force_reply is False


def _nutrition_bridge(tmp_path: Path) -> PhysiqueCheckinBridge:
    config = PhysiqueCheckinConfig.from_extra({
        "physique_checkin": {
            "enabled": True,
            "owner_id": "customer-ux",
            "chat_id": "customer-dm",
            "topic_id": "0",
            "expires_seconds": 3600,
        }
    })
    assert config is not None
    return PhysiqueCheckinBridge(
        config,
        service=WizardService.for_standalone(tmp_path / "wizard"),
        binding_path=tmp_path / "bindings.json",
        customer_key="customer-ux",
    )


def _start_nutrition_checkin(
    bridge: PhysiqueCheckinBridge,
) -> tuple[str, WizardReply]:
    launcher = bridge.open_launcher(
        "nutrition_daily",
        message_id="44",
        now_epoch=NOW,
    )
    assert launcher.callback_data is not None
    callback = CallbackData.parse(launcher.callback_data)
    assert callback is not None
    started = bridge.handle_callback(
        launcher.callback_data,
        "customer-ux",
        "customer-dm",
        "0",
        "44",
        now_epoch=NOW,
    )
    return callback.session_id, started


def _answer_text(
    bridge: PhysiqueCheckinBridge,
    value: str,
) -> WizardReply:
    reply = bridge.handle_text(
        value,
        "customer-ux",
        "customer-dm",
        "0",
        now_epoch=NOW,
    )
    assert reply is not None
    return reply


def _click(
    bridge: PhysiqueCheckinBridge,
    session_id: str,
    action: str,
) -> WizardReply:
    prompt = bridge.active_prompt()
    assert prompt is not None
    callback_data = next(
        data
        for _, data in prompt.buttons
        if (parsed := CallbackData.parse(data)) is not None
        and parsed.session_id == session_id
        and parsed.action == action
    )
    return bridge.handle_callback(
        callback_data,
        "customer-ux",
        "customer-dm",
        "0",
        "44",
        now_epoch=NOW,
    )


def test_daily_checkin_recovers_at_the_invalid_field(tmp_path: Path) -> None:
    bridge = _nutrition_bridge(tmp_path)
    _, started = _start_nutrition_checkin(bridge)
    assert started.accepted is True

    _ = _answer_text(bridge, "70.2")
    _ = _answer_text(bridge, "2300")
    invalid = _answer_text(bridge, "대충 먹었어요")

    assert invalid.accepted is False
    assert invalid.prompt is not None
    snapshot = bridge.active_checkin_snapshot()
    assert snapshot is not None
    assert snapshot["step"] == "macros"

    corrected = _answer_text(bridge, "280 150 65")
    assert corrected.accepted is True
    snapshot = bridge.active_checkin_snapshot()
    assert snapshot is not None
    assert snapshot["step"] == "meals"


def test_daily_checkin_accepts_common_korean_units(tmp_path: Path) -> None:
    bridge = _nutrition_bridge(tmp_path)
    _, started = _start_nutrition_checkin(bridge)
    assert started.accepted is True

    answers = (
        ("70.2kg", "calories"),
        ("2300 kcal", "macros"),
        (
            "탄수화물 280g, 단백질 150g, 지방 65g",
            "meals",
        ),
        ("아침 달걀과 밥 / 점심 닭가슴살과 밥", "water"),
        ("2.5리터", "sleep_duration"),
        ("7.5시간", "sleep_quality"),
    )
    for answer, next_step in answers:
        reply = _answer_text(bridge, answer)
        assert reply.accepted is True
        snapshot = bridge.active_checkin_snapshot()
        assert snapshot is not None
        assert snapshot["step"] == next_step


def test_daily_checkin_can_finish_without_typing_a_workout(tmp_path: Path) -> None:
    bridge = _nutrition_bridge(tmp_path)
    session_id, _ = _start_nutrition_checkin(bridge)

    for answer in (
        "70.2",
        "2300",
        "280 150 65",
        "아침 달걀과 밥 / 점심 닭가슴살과 밥 / 저녁 국밥",
        "2.5",
        "7.5",
    ):
        assert _answer_text(bridge, answer).accepted is True
    assert _click(bridge, session_id, "a4").accepted is True
    assert _click(bridge, session_id, "a3").accepted is True
    snapshot = bridge.active_checkin_snapshot()
    assert snapshot is not None
    answers = snapshot["answers"]
    assert isinstance(answers, dict)
    assert answers["digestion"] == "normal"
    assert _click(bridge, session_id, "a4").accepted is True

    workout = _answer_text(bridge, "식욕 3, 스트레스 2")
    assert workout.prompt is not None
    callbacks = tuple(CallbackData.parse(data) for _, data in workout.prompt.buttons)
    assert tuple(callback.action for callback in callbacks if callback) == (
        "a0",
        "a1",
    )

    assert _click(bridge, session_id, "a0").accepted is True
    snapshot = bridge.active_checkin_snapshot()
    assert snapshot is not None
    assert snapshot["step"] == "optional_note"
    answers = snapshot["answers"]
    assert isinstance(answers, dict)
    assert answers["training_summary"] == "no_workout"
    summary = _click(bridge, session_id, "a0")
    assert summary.accepted is True
    assert summary.prompt is not None
    assert "normal" not in summary.prompt.text.casefold()
    assert "no_workout" not in summary.prompt.text.casefold()
    saved = _click(bridge, session_id, "a0")

    assert saved.accepted is True
    assert bridge.active_is_finalized() is True


def test_customer_completion_copy_hides_internal_markers() -> None:
    rendered = TelegramAdapter.nutrition_daily_customer_text(
        {
            "answers": {
                "bodyweight": "70.2",
                "calories": "2300",
                "macros": "280 150 65",
                "sleep_duration": "7.5",
                "sleep_quality": "4",
                "condition": "4",
                "digestion": "normal",
                "training_summary": "no_workout",
            }
        }
    ).casefold()

    assert "normal" not in rendered
    assert "no_workout" not in rendered
