"""Тесты секционных tool'ов чата (#957, Step 2) — PURE срезы report_dict, без БД/recompute.

Покрываем:
  • tool_specs: 5 спек, валидная OpenAI-форма, без параметров.
  • executors: достают нужную секцию ВЕРБАТИМ; числа не пересчитываются.
  • robust: отсутствует секция → маркер «недоступно», НЕ KeyError.
  • execute_tool/sections_for_tool: реестр имя→executor + provenance-секции.
  • неизвестное имя tool'а: безопасный маркер + пустые секции (не падаем).
"""

from __future__ import annotations

from typing import Any

from app.services.chat import tools

_EXPECTED_TOOLS = {
    "get_exec_summary",
    "get_product_recommendation",
    "get_forecast",
    "get_risks",
    "get_scenarios",
    "get_parcel_info",
}


def _report() -> dict[str, Any]:
    """Узнаваемый отчёт (форма SiteFinderReport.as_dict())."""
    return {
        "exec_summary": {
            "headline": "Комфорт-класс",
            "key_numbers": {"цена_руб_м2": 250000},
            "overall_confidence": "medium",
        },
        "future_market": {
            "forecasts_by_horizon": [{"horizon": 12}, {"horizon": 24}],
            "future_supply": {"pressure": 0.4},
        },
        "product_tz": {"obj_class": "комфорт", "mix": [{"fmt": "2k"}]},
        "scoring": {"special_indices": {"indices": {"cannibalization": {"value": 0.31}}}},
        "confidence": {"level": "medium", "factors": {"deals": "ok"}},
        "scenarios": {"by_scenario": {"base": {}, "conservative": {}}},
        "parcel_context": {
            "address": "Екатеринбург, ул. Ленина, 1",
            "zone_code": "Ж-4",
            "zone_name": "Зона многоэтажной застройки",
            "max_floors": 25,
            "has_zouit": True,
            "zouit_zone_names": ["Приаэродромная территория"],
        },
    }


# ── Спеки ───────────────────────────────────────────────────────────────────────


def test_tool_specs_cover_all_six() -> None:
    specs = tools.tool_specs()
    assert len(specs) == 6
    assert {s["function"]["name"] for s in specs} == _EXPECTED_TOOLS


def test_tool_specs_are_valid_openai_function_shape() -> None:
    for spec in tools.tool_specs():
        assert spec["type"] == "function"
        fn = spec["function"]
        assert isinstance(fn["name"], str) and fn["name"]
        assert isinstance(fn["description"], str) and fn["description"]
        params = fn["parameters"]
        assert params["type"] == "object"
        # Секционные tool'ы без аргументов (модель не управляет вычислениями).
        assert params["properties"] == {}
        assert params["additionalProperties"] is False


# ── Executors: верные срезы, числа вербатим ──────────────────────────────────────


def test_exec_summary_slice_is_verbatim() -> None:
    out = tools.get_exec_summary(_report())
    assert out["headline"] == "Комфорт-класс"
    # Число берётся как есть, не пересчитывается.
    assert out["key_numbers"]["цена_руб_м2"] == 250000


def test_product_recommendation_slice() -> None:
    out = tools.get_product_recommendation(_report())
    assert out["obj_class"] == "комфорт"
    assert out["mix"] == [{"fmt": "2k"}]


def test_forecast_slice() -> None:
    out = tools.get_forecast(_report())
    assert len(out["forecasts_by_horizon"]) == 2
    assert out["future_supply"]["pressure"] == 0.4


def test_risks_merges_scoring_and_confidence() -> None:
    out = tools.get_risks(_report())
    assert set(out) == {"scoring", "confidence"}
    indices = out["scoring"]["special_indices"]["indices"]
    assert indices["cannibalization"]["value"] == 0.31
    assert out["confidence"]["level"] == "medium"


def test_scenarios_slice() -> None:
    out = tools.get_scenarios(_report())
    assert set(out["by_scenario"]) == {"base", "conservative"}


def test_parcel_info_slice_is_verbatim() -> None:
    out = tools.get_parcel_info(_report())
    assert out["zone_code"] == "Ж-4"
    assert out["zone_name"] == "Зона многоэтажной застройки"
    assert out["max_floors"] == 25
    assert out["zouit_zone_names"] == ["Приаэродромная территория"]


def test_parcel_info_missing_returns_marker() -> None:
    # Паспорт участка не влит (analyze-рана не было) → маркер «недоступно», не KeyError.
    assert tools.get_parcel_info({}) == {"available": False}
    assert tools.get_parcel_info({"parcel_context": {}}) == {"available": False}


# ── Robust: отсутствующая/пустая секция → маркер, НЕ KeyError ─────────────────────


def test_missing_section_returns_not_available_marker() -> None:
    assert tools.get_exec_summary({}) == {"available": False}
    assert tools.get_forecast({}) == {"available": False}
    assert tools.get_scenarios({}) == {"available": False}


def test_risks_robust_to_missing_subsections() -> None:
    out = tools.get_risks({})
    assert out == {
        "scoring": {"available": False},
        "confidence": {"available": False},
    }


def test_empty_section_dict_treated_as_unavailable() -> None:
    # Пустой dict секции → маркер (нечего показывать), а не «{}» как валидные данные.
    assert tools.get_product_recommendation({"product_tz": {}}) == {"available": False}


# ── Реестр: execute_tool / sections_for_tool / is_known_tool ─────────────────────


def test_execute_tool_dispatches_by_name() -> None:
    out = tools.execute_tool("get_exec_summary", _report())
    assert out["headline"] == "Комфорт-класс"


def test_execute_unknown_tool_returns_marker_not_raise() -> None:
    assert tools.execute_tool("get_nonexistent", _report()) == {"available": False}


def test_sections_for_tool_provenance() -> None:
    assert tools.sections_for_tool("get_exec_summary") == ("exec_summary",)
    assert tools.sections_for_tool("get_risks") == ("scoring", "confidence")
    assert tools.sections_for_tool("get_parcel_info") == ("parcel_context",)
    assert tools.sections_for_tool("get_nonexistent") == ()


def test_is_known_tool() -> None:
    assert tools.is_known_tool("get_forecast") is True
    assert tools.is_known_tool("nope") is False


def test_executors_do_not_mutate_report() -> None:
    report = _report()
    before = dict(report["exec_summary"])
    tools.get_exec_summary(report)
    assert report["exec_summary"] == before
