"""Юнит-тесты роутинга intent'ов + рендеринга RU-ответов чата (#957, Step 1, БЕЗ LLM).

Покрывает:
  • route_intent — явный intent уважается; keyword-матч RU-вопроса; нет матча → unknown.
  • render_answer — корректная секция(и) на intent; числа ВЕРБАТИМ из отчёта (не
    фабрикуются); advisory-оговорка в каждом ответе; graceful на отсутствующей секции
    («данных по X нет», без KeyError); unknown → меню тем.

Никакой БД — работаем с фикстурой-dict (форма SiteFinderReport.as_dict()).
"""

from __future__ import annotations

from typing import Any

import pytest

from app.schemas.chat import ChatIntent
from app.services.chat.intents import _ADVISORY_NOTE, render_answer, route_intent

# Разделитель тысяч — неразрывный пробел (зеркало ru-locale, _NBSP в intents.py).
_NBSP = " "


def _full_report() -> dict[str, Any]:
    """Полный 8-секционный отчёт (форма SiteFinderReport.as_dict()) с числами-маркерами.

    Числа подобраны узнаваемыми (7654321, 250000), чтобы тест мог утверждать: они
    приходят ИМЕННО из отчёта, а не выдуманы рендерером.
    """
    return {
        "schema_version": "1.0",
        "advisory": True,
        "exec_summary": {
            "headline": "Комфорт-класс, 2-3-комнатные",
            "verdict": "Участок подходит под жильё комфорт-класса.",
            "key_numbers": {"площадь_м2": 7654321, "цена_руб_м2": 250000},
            "overall_confidence": "medium",
        },
        "market_now": {
            "market_metrics": {"absorption": 12.0},
            "competitors": [{"name": "ЖК А"}],
            "supply_layers": None,
            "summary": "Рынок активен.",
        },
        "future_market": {
            "forecasts_by_horizon": [{"horizon": 12}, {"horizon": 24}],
            "future_supply": {"pressure": 0.4},
            "future_competitors": [],
            "scenarios_summary": {"spread": "wide"},
            "summary": "Ожидается дефицит на горизонте 12 мес.",
        },
        "product_tz": {
            "obj_class": "комфорт",
            "mix": [{"fmt": "1k"}, {"fmt": "2k"}, {"fmt": "3k"}],
            "commercial": {"gla_m2": 1200},
            "usp": [{"usp": "видовые квартиры"}],
            "reasons": [{"why": "дефицит формата"}, {"why": "ценовое окно"}],
            "summary": "Рекомендуется комфорт-класс с акцентом на 2-3-комнатные.",
        },
        "scenarios": {
            "by_scenario": {
                "conservative": {"npv": 1},
                "base": {"npv": 2},
                "aggressive": {"npv": 3},
            },
            "summary": "Разброс сценариев умеренный.",
        },
        "scoring": {
            "product_scores": {"overall": 0.72},
            "special_indices": {
                "indices": {
                    "launch_window": {"value": None, "label": "18 мес"},
                    "cannibalization": {"value": 0.31, "label": None},
                    "competitor_strength": {"value": None, "label": None},
                }
            },
            "overall": 0.72,
        },
        "confidence": {
            "level": "medium",
            "rationale": "Данные по сделкам полные, прогноз спроса — прокси.",
            "factors": {"deals": "ok", "supply": "proxy"},
        },
        "meta": {
            "cad_num": "66:41:0204016:10",
            "district": "Екатеринбург",
            "segment": {},
            "horizons": [12, 24],
            "schema_version": "1.0",
        },
    }


# ── route_intent ─────────────────────────────────────────────────────────────────


def test_route_intent_explicit_wins_over_text() -> None:
    """Явный intent уважается, даже если текст намекает на другое."""
    assert route_intent("какие тут риски?", ChatIntent.summary) == ChatIntent.summary


@pytest.mark.parametrize(
    ("message", "expected"),
    [
        ("Что здесь строить?", ChatIntent.what_to_build),
        ("Какой продукт рекомендуете?", ChatIntent.what_to_build),
        ("Почему такой прогноз?", ChatIntent.why_forecast),
        ("На чём основан прогноз?", ChatIntent.why_forecast),
        ("Какие риски у участка?", ChatIntent.risks),
        ("Есть ли каннибализация портфеля?", ChatIntent.risks),
        ("Покажи сценарии развития", ChatIntent.scenarios),
        ("Дай краткое резюме по участку", ChatIntent.summary),
        ("Какой вердикт?", ChatIntent.summary),
    ],
)
def test_route_intent_keyword_match(message: str, expected: ChatIntent) -> None:
    """Keyword/regex-матч RU-вопроса → правильный intent."""
    assert route_intent(message, None) == expected


def test_route_intent_no_match_returns_unknown() -> None:
    """Нераспознанный вопрос → unknown."""
    assert route_intent("Какая сегодня погода в Москве?", None) == ChatIntent.unknown


def test_route_intent_empty_message_unknown() -> None:
    """Пустое сообщение → unknown (не падаем)."""
    assert route_intent("   ", None) == ChatIntent.unknown


# ── render_answer: секции + числа из отчёта ──────────────────────────────────────


def test_render_summary_uses_exec_summary_and_verbatim_numbers() -> None:
    """summary тянет exec_summary; числа приходят ВЕРБАТИМ из отчёта (форматированы)."""
    answer, sections = render_answer(ChatIntent.summary, _full_report())
    assert sections == ["exec_summary"]
    assert "Комфорт-класс, 2-3-комнатные" in answer
    # Числа приходят ВЕРБАТИМ из отчёта (тысячи — неразрывным пробелом, ru-locale).
    assert f"7{_NBSP}654{_NBSP}321" in answer
    assert f"250{_NBSP}000" in answer
    assert "средняя" in answer  # overall_confidence=medium → RU
    assert _ADVISORY_NOTE in answer


def test_render_what_to_build_uses_product_tz_and_headline() -> None:
    """what_to_build тянет product_tz + exec_summary.headline."""
    answer, sections = render_answer(ChatIntent.what_to_build, _full_report())
    assert "product_tz" in sections
    assert "exec_summary" in sections
    assert "комфорт" in answer
    assert _ADVISORY_NOTE in answer


def test_render_why_forecast_uses_future_and_reasons_and_confidence() -> None:
    """why_forecast тянет future_market + product_tz.reasons + confidence.rationale."""
    answer, sections = render_answer(ChatIntent.why_forecast, _full_report())
    assert "future_market" in sections
    assert "product_tz" in sections
    assert "confidence" in sections
    assert "2 горизонт" in answer  # 2 forecasts_by_horizon
    assert "прокси" in answer  # из confidence.rationale
    assert _ADVISORY_NOTE in answer


def test_render_risks_uses_special_indices_and_confidence() -> None:
    """risks тянет scoring.special_indices (вкл. каннибализацию) + confidence."""
    answer, sections = render_answer(ChatIntent.risks, _full_report())
    assert "scoring" in sections
    assert "confidence" in sections
    assert "каннибализация портфеля" in answer
    assert "0,31" in answer  # value=0.31 → RU-десятичная из отчёта
    assert "18 мес" in answer  # launch_window label из отчёта
    assert _ADVISORY_NOTE in answer


def test_render_risks_skips_uncomputed_index() -> None:
    """Индекс с value=None и label=None пропускается честно (не фабрикуем)."""
    answer, _ = render_answer(ChatIntent.risks, _full_report())
    # competitor_strength был None/None → его RU-метки в ответе быть не должно.
    assert "сила конкурентов" not in answer


def test_render_scenarios_uses_scenarios_section() -> None:
    """scenarios тянет by_scenario + summary, метки сценариев → RU."""
    answer, sections = render_answer(ChatIntent.scenarios, _full_report())
    assert sections == ["scenarios"]
    assert "консервативный" in answer
    assert "базовый" in answer
    assert "агрессивный" in answer
    assert _ADVISORY_NOTE in answer


def test_render_unknown_returns_menu() -> None:
    """unknown → меню поддерживаемых тем, секции не используются."""
    answer, sections = render_answer(ChatIntent.unknown, _full_report())
    assert sections == []
    assert "что здесь строить" in answer.lower()
    assert "сценарии" in answer.lower()
    assert _ADVISORY_NOTE in answer


# ── render_answer: graceful на частичном/пустом отчёте ───────────────────────────


@pytest.mark.parametrize(
    "intent",
    [
        ChatIntent.summary,
        ChatIntent.what_to_build,
        ChatIntent.why_forecast,
        ChatIntent.risks,
        ChatIntent.scenarios,
        ChatIntent.unknown,
    ],
)
def test_render_empty_report_never_raises_and_has_advisory(intent: ChatIntent) -> None:
    """Пустой отчёт ({}) — никакого KeyError, ответ непустой + advisory-оговорка."""
    answer, sections = render_answer(intent, {})
    assert isinstance(answer, str)
    assert answer.strip()
    assert _ADVISORY_NOTE in answer
    assert isinstance(sections, list)


def test_render_summary_missing_section_says_honestly() -> None:
    """Нет exec_summary → честное «данных по разделу резюме нет», не выдумка."""
    answer, sections = render_answer(ChatIntent.summary, {"market_now": {}})
    assert sections == []
    assert "Данных по разделу «резюме» в отчёте нет." in answer


def test_render_risks_missing_special_indices_says_honestly() -> None:
    """Нет scoring.special_indices → честное сообщение об отсутствии §25-индексов."""
    answer, _ = render_answer(ChatIntent.risks, {"scoring": {"product_scores": {}}})
    assert "специальные индексы §25" in answer.lower()


def test_render_does_not_fabricate_numbers_on_empty() -> None:
    """На пустом отчёте в ответе нет «выдуманных» числовых маркеров фикстуры."""
    answer, _ = render_answer(ChatIntent.summary, {})
    assert f"7{_NBSP}654{_NBSP}321" not in answer
    assert f"250{_NBSP}000" not in answer
