"""Unit-тесты §13/§22 Markdown-экспортёра (#959, EPIC export) — `render_report_markdown`.

Чистые тесты БЕЗ БД/сети (экспортёр только ПОТРЕБЛЯЕТ уже-собранный отчёт):
  • реальный fixture-отчёт (frontend mock `parcel-forecast.json` → `report`) рендерится
    в Markdown с заголовками всех секций, advisory-дисклеймером и значением дефицита;
  • экспортёр принимает КАК `SiteFinderReport`-инстанс, ТАК и его `as_dict()`-словарь;
  • пустой / частичный / мусорный отчёт → валидный Markdown БЕЗ падения (graceful);
  • pipe в данных экранируется (не ломает GFM-таблицу).

Детерминированно, без LLM. DATABASE_URL выставляем до импорта app-модулей (зеркало
test_report_pdf.py) — на случай side-effect'ов импорта пакета.
"""

from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any

os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

from app.services.exporters.report_md import (
    _ADVISORY_DISCLAIMER,
    _TITLE_CONFIDENCE,
    _TITLE_FUTURE_MARKET,
    _TITLE_MARKET_NOW,
    _TITLE_PRODUCT_TZ,
    _TITLE_SCENARIOS,
    _TITLE_SCORING,
    _TITLE_SUMMARY,
    render_report_markdown,
)
from app.services.forecasting.report import (
    ReportExecSummary,
    ReportMeta,
    SiteFinderReport,
)

# Реальный fixture: frontend mock §22-форсайта — берём вложенный `report` (форма #987
# `as_dict()`). Так тест не разойдётся с реальной формой отчёта.
_FIXTURE_PATH = (
    Path(__file__).resolve().parents[4]
    / "frontend"
    / "src"
    / "lib"
    / "mocks"
    / "parcel-forecast.json"
)


def _fixture_report() -> dict[str, Any]:
    """`report`-словарь из frontend-мока §22 (форма SiteFinderReport.as_dict())."""
    data = json.loads(_FIXTURE_PATH.read_text(encoding="utf-8"))
    report = data["report"]
    assert isinstance(report, dict)
    return report


_EXPECTED_TITLES: tuple[str, ...] = (
    _TITLE_SUMMARY,
    _TITLE_MARKET_NOW,
    _TITLE_FUTURE_MARKET,
    _TITLE_SCENARIOS,
    _TITLE_PRODUCT_TZ,
    _TITLE_SCORING,
    _TITLE_CONFIDENCE,
)


# ── Реальный fixture-отчёт: структура + ключевые значения ───────────────────────


class TestFixtureReportRender:
    def test_returns_non_empty_str(self) -> None:
        md = render_report_markdown(_fixture_report())
        assert isinstance(md, str)
        assert len(md) > 200

    def test_h1_and_advisory_disclaimer_present(self) -> None:
        md = render_report_markdown(_fixture_report())
        # H1 заголовок документа.
        assert md.lstrip().startswith("# ")
        # Advisory-дисклеймер обязателен (отчёт советующий).
        assert _ADVISORY_DISCLAIMER in md

    def test_all_section_headers_present(self) -> None:
        md = render_report_markdown(_fixture_report())
        for title in _EXPECTED_TITLES:
            assert f"## {title}" in md, f"отсутствует заголовок секции «{title}»"

    def test_meta_cad_num_present(self) -> None:
        md = render_report_markdown(_fixture_report())
        # Кадастр из meta фикстуры попадает в карточку.
        assert "66:41:0702048:27" in md

    def test_deficit_value_present(self) -> None:
        # Сводный индекс дефицита (−1.0) из exec_summary/forecasts должен быть в MD.
        report = _fixture_report()
        deficit = report["exec_summary"]["key_numbers"]["deficit_index"]
        assert deficit is not None
        # _fmt округляет float до 3 знаков → "-1.0".
        assert str(round(float(deficit), 3)) in md_value(report)

    def test_headline_present(self) -> None:
        report = _fixture_report()
        headline = report["exec_summary"]["headline"]
        md = render_report_markdown(report)
        # Заголовок-вердикт присутствует (bold).
        assert headline.split(".")[0] in md

    def test_gfm_table_separator_present(self) -> None:
        # Хотя бы одна GFM-таблица (разделитель шапки) есть в выводе.
        md = render_report_markdown(_fixture_report())
        assert "| --- |" in md


def md_value(report: dict[str, Any]) -> str:
    """Helper: отрендерить отчёт (сахар для inline-ассертов)."""
    return render_report_markdown(report)


# ── Вход: dataclass-инстанс ИЛИ as_dict()-словарь ───────────────────────────────


class TestInputForms:
    def test_accepts_dataclass_instance(self) -> None:
        report = SiteFinderReport(
            exec_summary=ReportExecSummary(headline="Строить комфорт-класс."),
            meta=ReportMeta(cad_num="66:41:0000000:1", horizons=[12]),
        )
        md = render_report_markdown(report)
        assert "Строить комфорт" in md
        assert "66:41:0000000:1" in md

    def test_dataclass_and_dict_equivalent(self) -> None:
        report = SiteFinderReport(
            exec_summary=ReportExecSummary(headline="Один заголовок."),
            meta=ReportMeta(cad_num="66:41:0000000:9"),
        )
        assert render_report_markdown(report) == render_report_markdown(report.as_dict())


# ── Graceful: пустой / частичный / мусорный вход → валидный MD без падения ───────


class TestGraceful:
    def test_empty_dict_does_not_crash(self) -> None:
        # Пустой отчёт {} → валидный MD (no KeyError), с дисклеймером и «нет данных».
        md = render_report_markdown({})
        assert isinstance(md, str)
        assert _ADVISORY_DISCLAIMER in md
        assert "нет данных" in md
        # Все секции всё равно отрисованы (стабильный контракт).
        for title in _EXPECTED_TITLES:
            assert f"## {title}" in md

    def test_default_report_does_not_crash(self) -> None:
        # Полностью дефолтный (пустой) SiteFinderReport — валидный MD.
        md = render_report_markdown(SiteFinderReport())
        assert _ADVISORY_DISCLAIMER in md
        assert "нет данных" in md

    def test_garbage_input_returns_valid_md(self) -> None:
        # Мусор (None / int / строка) → нормализуется в {} → валидный MD, не падает.
        for junk in (None, 123, "not a report", []):
            md = render_report_markdown(junk)
            assert isinstance(md, str)
            assert _ADVISORY_DISCLAIMER in md

    def test_partial_report_some_sections(self) -> None:
        report = SiteFinderReport(
            exec_summary=ReportExecSummary(headline="Тонкий анализ — только заголовок."),
            meta=ReportMeta(cad_num="66:41:0000000:2", horizons=[12]),
        )
        md = render_report_markdown(report)
        assert "Тонкий анализ" in md
        assert "66:41:0000000:2" in md
        # Незаполненные секции рисуют «нет данных», но MD валиден.
        assert "нет данных" in md


# ── Безопасность таблиц: pipe в данных не ломает GFM-разметку ───────────────────


class TestPipeEscaping:
    def test_pipe_in_value_escaped(self) -> None:
        report = SiteFinderReport(
            exec_summary=ReportExecSummary(
                key_numbers={"weird|key": "value|with|pipes"},
            ),
        )
        md = render_report_markdown(report)
        # Сырой неэкранированный pipe из значения не должен утечь — он экранирован.
        assert "value\\|with\\|pipes" in md
