"""Тесты 3 эндпоинтов полного PDF-отчёта в parcels API (эпик #2259 PR-D).

Покрывает:
  • POST /{cad}/report: нет analyze-рана → 404; enqueue → 202 {status:"building"};
    готовый кэш (файл на диске) → 200 {status:"ready"} без повторного enqueue;
    провал брокера → 503.
  • GET /{cad}/report/status: none (нет analyze) / building (analyze есть, файла нет) /
    ready (метадата-ран + файл на диске).
  • GET /{cad}/report/download: файл на диске → 200 application/pdf + attachment;
    отчёт не готов → 404.

Стратегия mock (зеркало test_parcels_forecast.py): DB через dependency_overrides,
`latest_run_for` патчим, Celery `.delay` — на реальной таске (lazy import в хендлере
резолвит атрибут в момент вызова). RBAC обходится settings.testing=True (conftest).
"""

from __future__ import annotations

import datetime as dt
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

from fastapi.testclient import TestClient

from app.main import app
from app.services.exporters.full_report_pdf import REPORT_SCHEMA_VERSION

_CAD = "66:41:0204016:10"
_ANALYZE_SV = "analyze-1.0"
_FORECAST_SV = "1.0"


def _override_db(db: MagicMock):
    def _get_db_override():
        yield db

    return _get_db_override


def _run(run_id: int, result: dict[str, Any] | None = None) -> MagicMock:
    r = MagicMock()
    r.id = run_id
    r.result = result or {}
    r.district = "Кировский"
    r.created_at = dt.datetime(2026, 7, 3, 10, 0, 0)
    return r


def _lrf_router(rows: dict[str, MagicMock | None]):
    """Фабрика side_effect для latest_run_for: row по kwargs['schema_version']."""

    def _side(db: Any, cad: str, *, schema_version: str | None = None) -> MagicMock | None:
        return rows.get(schema_version)

    return _side


# ── POST /{cad}/report ────────────────────────────────────────────────────────


def test_post_report_no_analyze_returns_404() -> None:
    from app.core.db import get_db

    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    try:
        with patch(
            "app.api.v1.parcels.latest_run_for", side_effect=_lrf_router({_ANALYZE_SV: None})
        ):
            client = TestClient(app)
            resp = client.post(f"/api/v1/parcels/{_CAD}/report")
            assert resp.status_code == 404, resp.text
            assert "анализа" in resp.json()["detail"]
    finally:
        app.dependency_overrides.clear()


def test_post_report_enqueues_and_returns_202() -> None:
    from app.core.db import get_db

    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    rows = {_ANALYZE_SV: _run(101), _FORECAST_SV: _run(202), REPORT_SCHEMA_VERSION: None}
    delay_mock = MagicMock()
    try:
        with (
            patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)),
            patch("app.workers.tasks.full_report.build_full_report_task.delay", delay_mock),
        ):
            # _cached_report_result: report-row None → кэша нет, delay вызывается.
            client = TestClient(app)
            resp = client.post(f"/api/v1/parcels/{_CAD}/report")
            assert resp.status_code == 202, resp.text
            body = resp.json()
            assert body["status"] == "building"
            assert body["analyze_run_at"] is not None
            # Отчёт ещё не собран → даты генерации PDF нет.
            assert body["report_generated_at"] is None
            delay_mock.assert_called_once_with(_CAD)
    finally:
        app.dependency_overrides.clear()


def test_post_report_cache_hit_returns_200_ready(tmp_path: Path) -> None:
    from app.core.db import get_db

    pdf = tmp_path / "report.pdf"
    pdf.write_bytes(b"%PDF-cached")
    report_row = _run(
        500,
        {
            "pdf_path": str(pdf),
            "analyze_run_id": 101,
            "forecast_run_id": 202,
            "generated_at": "2026-07-03T09:00:00+00:00",
            "size_bytes": 10,
        },
    )
    rows = {_ANALYZE_SV: _run(101), _FORECAST_SV: _run(202), REPORT_SCHEMA_VERSION: report_row}
    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    delay_mock = MagicMock()
    try:
        with (
            patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)),
            patch("app.workers.tasks.full_report.build_full_report_task.delay", delay_mock),
        ):
            client = TestClient(app)
            resp = client.post(f"/api/v1/parcels/{_CAD}/report")
            assert resp.status_code == 200, resp.text
            body = resp.json()
            assert body["status"] == "ready"
            # Fast-path: POST-ответ несёт дату генерации PDF из метадаты рана (без GET /status).
            assert body["report_generated_at"] == "2026-07-03T09:00:00+00:00"
            delay_mock.assert_not_called()  # кэш есть → не enqueue'им
    finally:
        app.dependency_overrides.clear()


def test_post_report_broker_down_returns_503() -> None:
    from app.core.db import get_db

    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    rows = {_ANALYZE_SV: _run(101), _FORECAST_SV: _run(202), REPORT_SCHEMA_VERSION: None}
    delay_mock = MagicMock(side_effect=RuntimeError("redis down"))
    try:
        with (
            patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)),
            patch("app.workers.tasks.full_report.build_full_report_task.delay", delay_mock),
        ):
            client = TestClient(app)
            resp = client.post(f"/api/v1/parcels/{_CAD}/report")
            assert resp.status_code == 503, resp.text
    finally:
        app.dependency_overrides.clear()


# ── GET /{cad}/report/status ──────────────────────────────────────────────────


def test_status_none_when_no_analyze() -> None:
    from app.core.db import get_db

    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    rows = {_ANALYZE_SV: None, _FORECAST_SV: None}
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
            client = TestClient(app)
            resp = client.get(f"/api/v1/parcels/{_CAD}/report/status")
            assert resp.status_code == 200, resp.text
            assert resp.json()["status"] == "none"
    finally:
        app.dependency_overrides.clear()


def test_status_building_when_analyze_but_no_file() -> None:
    from app.core.db import get_db

    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    rows = {_ANALYZE_SV: _run(101), _FORECAST_SV: _run(202), REPORT_SCHEMA_VERSION: None}
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
            client = TestClient(app)
            resp = client.get(f"/api/v1/parcels/{_CAD}/report/status")
            assert resp.status_code == 200, resp.text
            body = resp.json()
            assert body["status"] == "building"
            assert body["analyze_run_at"] is not None
    finally:
        app.dependency_overrides.clear()


def test_status_ready_when_file_on_disk(tmp_path: Path) -> None:
    from app.core.db import get_db

    pdf = tmp_path / "report.pdf"
    pdf.write_bytes(b"%PDF")
    report_row = _run(
        500,
        {
            "pdf_path": str(pdf),
            "analyze_run_id": 101,
            "forecast_run_id": 202,
            "generated_at": "2026-07-03T09:00:00+00:00",
            "size_bytes": 4,
        },
    )
    rows = {_ANALYZE_SV: _run(101), _FORECAST_SV: _run(202), REPORT_SCHEMA_VERSION: report_row}
    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
            client = TestClient(app)
            resp = client.get(f"/api/v1/parcels/{_CAD}/report/status")
            assert resp.status_code == 200, resp.text
            body = resp.json()
            assert body["status"] == "ready"
            assert body["report_generated_at"] == "2026-07-03T09:00:00+00:00"
    finally:
        app.dependency_overrides.clear()


# ── GET /{cad}/report/download ────────────────────────────────────────────────


def test_download_returns_pdf_when_ready(tmp_path: Path) -> None:
    from app.core.db import get_db

    pdf = tmp_path / "report.pdf"
    pdf.write_bytes(b"%PDF-1.7 payload")
    # generated_at — ВЧЕРАШНЯЯ дата (cache-hit со вчера): имя файла должно взять ЕЁ,
    # НЕ today (иначе имя врёт про дату сборки — ревью PR-D п.3).
    report_row = _run(
        500,
        {
            "pdf_path": str(pdf),
            "analyze_run_id": 101,
            "forecast_run_id": 202,
            "generated_at": "2026-07-01T09:00:00+00:00",
            "size_bytes": 16,
        },
    )
    rows = {REPORT_SCHEMA_VERSION: report_row}
    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
            client = TestClient(app)
            resp = client.get(f"/api/v1/parcels/{_CAD}/report/download")
            assert resp.status_code == 200, resp.text
            assert resp.headers["content-type"] == "application/pdf"
            cd = resp.headers["content-disposition"]
            assert "attachment;" in cd
            assert cd.endswith('.pdf"')
            assert f"gendesign_report_{_CAD.replace(':', '_')}_" in cd
            # Дата в имени — из метадаты (2026-07-01), НЕ today.
            assert "2026-07-01" in cd
            assert resp.content == b"%PDF-1.7 payload"
    finally:
        app.dependency_overrides.clear()


def test_download_404_when_not_ready() -> None:
    from app.core.db import get_db

    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    rows = {REPORT_SCHEMA_VERSION: None}
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
            client = TestClient(app)
            resp = client.get(f"/api/v1/parcels/{_CAD}/report/download")
            assert resp.status_code == 404, resp.text
            assert "готов" in resp.json()["detail"]
    finally:
        app.dependency_overrides.clear()


# ── GET /{cad}/report/download?format=docx (#2259 PR-F) ─────────────────────────


def test_download_docx_returns_word_when_ready(tmp_path: Path) -> None:
    """format=docx + docx_path на диске → 200 + Word media-type + .docx filename."""
    from app.core.db import get_db

    pdf = tmp_path / "report.pdf"
    pdf.write_bytes(b"%PDF-1.7 payload")
    docx = tmp_path / "report.docx"
    docx.write_bytes(b"PK\x03\x04 fake-docx")
    report_row = _run(
        500,
        {
            "pdf_path": str(pdf),
            "docx_path": str(docx),
            "analyze_run_id": 101,
            "forecast_run_id": 202,
            "generated_at": "2026-07-01T09:00:00+00:00",
            "size_bytes": 16,
            "docx_size_bytes": 12,
        },
    )
    rows = {REPORT_SCHEMA_VERSION: report_row}
    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
            client = TestClient(app)
            resp = client.get(f"/api/v1/parcels/{_CAD}/report/download?format=docx")
            assert resp.status_code == 200, resp.text
            assert resp.headers["content-type"] == (
                "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
            )
            cd = resp.headers["content-disposition"]
            assert "attachment;" in cd
            assert cd.endswith('.docx"')
            assert "2026-07-01" in cd
            assert resp.content == b"PK\x03\x04 fake-docx"
    finally:
        app.dependency_overrides.clear()


def test_download_default_format_is_pdf(tmp_path: Path) -> None:
    """format не задан → default pdf (даже если docx_path тоже есть)."""
    from app.core.db import get_db

    pdf = tmp_path / "report.pdf"
    pdf.write_bytes(b"%PDF-1.7 payload")
    docx = tmp_path / "report.docx"
    docx.write_bytes(b"PK\x03\x04 fake-docx")
    report_row = _run(
        500,
        {
            "pdf_path": str(pdf),
            "docx_path": str(docx),
            "analyze_run_id": 101,
            "forecast_run_id": 202,
            "generated_at": "2026-07-01T09:00:00+00:00",
            "size_bytes": 16,
        },
    )
    rows = {REPORT_SCHEMA_VERSION: report_row}
    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
            client = TestClient(app)
            resp = client.get(f"/api/v1/parcels/{_CAD}/report/download")
            assert resp.status_code == 200, resp.text
            assert resp.headers["content-type"] == "application/pdf"
            assert resp.content == b"%PDF-1.7 payload"
    finally:
        app.dependency_overrides.clear()


def test_download_docx_404_when_legacy_run_has_no_docx(tmp_path: Path) -> None:
    """Старый ран (собран до PR-F) без docx_path → format=docx 404 «пересоберите»."""
    from app.core.db import get_db

    pdf = tmp_path / "report.pdf"
    pdf.write_bytes(b"%PDF-1.7 payload")
    # Метадата без docx_path — legacy отчёт, собранный до PR-F.
    report_row = _run(
        500,
        {
            "pdf_path": str(pdf),
            "analyze_run_id": 101,
            "forecast_run_id": 202,
            "generated_at": "2026-07-01T09:00:00+00:00",
            "size_bytes": 16,
        },
    )
    rows = {REPORT_SCHEMA_VERSION: report_row}
    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
            client = TestClient(app)
            resp = client.get(f"/api/v1/parcels/{_CAD}/report/download?format=docx")
            assert resp.status_code == 404, resp.text
            assert "пересоберите" in resp.json()["detail"]
    finally:
        app.dependency_overrides.clear()


def test_download_docx_404_when_file_missing(tmp_path: Path) -> None:
    """docx_path в метадате есть, но файл на диске исчез → format=docx 404."""
    from app.core.db import get_db

    pdf = tmp_path / "report.pdf"
    pdf.write_bytes(b"%PDF")
    report_row = _run(
        500,
        {
            "pdf_path": str(pdf),
            "docx_path": str(tmp_path / "gone.docx"),  # файла нет
            "analyze_run_id": 101,
            "forecast_run_id": 202,
            "generated_at": "2026-07-01T09:00:00+00:00",
            "size_bytes": 4,
        },
    )
    rows = {REPORT_SCHEMA_VERSION: report_row}
    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
            client = TestClient(app)
            resp = client.get(f"/api/v1/parcels/{_CAD}/report/download?format=docx")
            assert resp.status_code == 404, resp.text
    finally:
        app.dependency_overrides.clear()


def test_download_invalid_format_returns_422() -> None:
    """Неизвестный format (не pdf/docx) → 422 (Literal-валидация FastAPI)."""
    from app.core.db import get_db

    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    try:
        client = TestClient(app)
        resp = client.get(f"/api/v1/parcels/{_CAD}/report/download?format=xlsx")
        assert resp.status_code == 422, resp.text
    finally:
        app.dependency_overrides.clear()


def test_download_head_returns_headers_only(tmp_path: Path) -> None:
    """HEAD на download — 200 с заголовками БЕЗ тела (preflight фронта, #2338).

    FastAPI не добавляет HEAD к @router.get автоматически — прод отдавал 405,
    HEAD-preflight абортился и UI молча не скачивал DOCX. Явный @router.head чинит.
    """
    from app.core.db import get_db

    pdf = tmp_path / "report.pdf"
    pdf.write_bytes(b"%PDF-1.7 payload")
    docx = tmp_path / "report.docx"
    docx.write_bytes(b"PK\x03\x04 fake-docx")
    report_row = _run(
        510,
        {
            "pdf_path": str(pdf),
            "docx_path": str(docx),
            "analyze_run_id": 101,
            "forecast_run_id": 202,
            "generated_at": "2026-07-01T09:00:00+00:00",
        },
    )
    rows = {REPORT_SCHEMA_VERSION: report_row}
    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
            client = TestClient(app)
            resp = client.head(f"/api/v1/parcels/{_CAD}/report/download?format=docx")
            assert resp.status_code == 200, resp.text
            assert resp.headers["content-type"] == (
                "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
            )
            assert resp.content == b""
    finally:
        app.dependency_overrides.clear()


def test_download_head_404_when_not_ready() -> None:
    """HEAD для участка без готового отчёта — 404 (симметрично GET)."""
    from app.core.db import get_db

    db = MagicMock()
    app.dependency_overrides[get_db] = _override_db(db)
    try:
        with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router({})):
            client = TestClient(app)
            resp = client.head(f"/api/v1/parcels/{_CAD}/report/download")
            assert resp.status_code == 404
    finally:
        app.dependency_overrides.clear()
