"""Тесты breadcrumb-логирования etl_newbuilding_crossload task (issue #2445 D6).

Раньше эта таска не имела ни try/except, ни breadcrumb-записи в отличие от всех
соседних nightly/weekly-тасок в этой директории (objective_etl.py и т.д.) —
сбой run_crossload() был виден только в Celery FAILURE + GlitchTip, но не в
operational-логе (objective_scrape_runs), который проверяет оператор.

Покрытие:
- success-path: run_id создаётся, run_crossload() вызывается с db=, breadcrumb
  помечается 'done' со счётчиками source_rows/upserted/skipped.
- disabled-путь (TRADEIN_DATABASE_URL пуст): breadcrumb 'done', error поясняет причину.
- failure-path: run_crossload() бросает исключение → breadcrumb 'failed' +
  error, задача ре-рейзит исключение (Celery должен уйти в FAILURE).
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch


def _make_db_mock(run_id: int = 7) -> MagicMock:
    """Mock SessionLocal() — INSERT ... RETURNING run_id возвращает run_id."""
    db = MagicMock()
    db.execute.return_value.scalar_one.return_value = run_id
    return db


def _capture_finish_run(mod: Any) -> tuple[list[dict[str, Any]], Any]:
    """Оборачивает mod._finish_run, чтобы записывать все вызовы (kwargs)."""
    calls: list[dict[str, Any]] = []
    orig = mod._finish_run

    def capture(_db: Any, _run_id: int, **kwargs: Any) -> None:
        calls.append({"run_id": _run_id, **kwargs})
        orig(_db, _run_id, **kwargs)

    return calls, capture


def test_success_writes_done_breadcrumb_with_counts() -> None:
    """Успешный прогон: run_id создан, breadcrumb → 'done' с source_rows/upserted/skipped."""
    from app.workers.tasks import etl_newbuilding_crossload as mod

    db = _make_db_mock(run_id=7)
    finish_calls, capture = _capture_finish_run(mod)

    with (
        patch.object(mod, "SessionLocal", return_value=db),
        patch.object(
            mod,
            "run_crossload",
            return_value={"source_rows": 100, "upserted": 95, "skipped": 5},
        ) as mock_run_crossload,
        patch.object(mod, "_finish_run", side_effect=capture),
    ):
        result = mod.etl_newbuilding_crossload.run(triggered_by="manual")

    assert result["run_id"] == 7
    assert result["source_rows"] == 100
    assert result["upserted"] == 95
    assert result["skipped"] == 5

    # run_crossload вызван с нашей db-сессией (переиспользуем транзакцию/savepoints).
    mock_run_crossload.assert_called_once_with(db=db)

    assert len(finish_calls) == 1
    assert finish_calls[0]["run_id"] == 7
    assert finish_calls[0]["status"] == "done"
    assert finish_calls[0]["error"] is None
    assert finish_calls[0]["rows_history"] == 100
    assert finish_calls[0]["rows_lots"] == 95
    assert finish_calls[0]["requests_count"] == 5

    db.close.assert_called_once()


def test_disabled_writes_done_breadcrumb_with_explanation() -> None:
    """TRADEIN_DATABASE_URL не задан → run_crossload вернул {"disabled": True};
    breadcrumb всё равно 'done' (ожидаемое поведение, не ошибка), error поясняет причину."""
    from app.workers.tasks import etl_newbuilding_crossload as mod

    db = _make_db_mock(run_id=11)
    finish_calls, capture = _capture_finish_run(mod)

    with (
        patch.object(mod, "SessionLocal", return_value=db),
        patch.object(mod, "run_crossload", return_value={"disabled": True}),
        patch.object(mod, "_finish_run", side_effect=capture),
    ):
        result = mod.etl_newbuilding_crossload.run(triggered_by="beat")

    assert result == {"run_id": 11, "disabled": True}
    assert len(finish_calls) == 1
    assert finish_calls[0]["status"] == "done"
    assert finish_calls[0]["error"] == "TRADEIN_DATABASE_URL not set"


def test_exception_writes_failed_breadcrumb_and_reraises() -> None:
    """run_crossload() бросает исключение (postgres timeout / schema drift) →
    breadcrumb помечается 'failed' с error-сообщением, задача ре-рейзит
    исключение (Celery FAILURE + GlitchTip продолжает ловить сбой)."""
    from app.workers.tasks import etl_newbuilding_crossload as mod

    db = _make_db_mock(run_id=13)
    finish_calls, capture = _capture_finish_run(mod)

    with (
        patch.object(mod, "SessionLocal", return_value=db),
        patch.object(mod, "run_crossload", side_effect=RuntimeError("tradein postgres timeout")),
        patch.object(mod, "_finish_run", side_effect=capture),
    ):
        try:
            mod.etl_newbuilding_crossload.run(triggered_by="beat")
            raised = False
        except RuntimeError as e:
            raised = True
            assert "tradein postgres timeout" in str(e)

    assert raised, "исключение должно ре-рейзиться, чтобы Celery ушёл в FAILURE"

    assert len(finish_calls) == 1
    assert finish_calls[0]["run_id"] == 13
    assert finish_calls[0]["status"] == "failed"
    assert "RuntimeError" in finish_calls[0]["error"]
    assert "tradein postgres timeout" in finish_calls[0]["error"]

    db.close.assert_called_once()


def test_breadcrumb_flush_failure_does_not_swallow_original_exception() -> None:
    """Если сам breadcrumb-flush (_finish_run) на failure-пути тоже падает —
    исходное исключение из run_crossload всё равно должно ре-рейзиться, не
    подавляться."""
    from app.workers.tasks import etl_newbuilding_crossload as mod

    db = _make_db_mock(run_id=21)

    def _boom_finish(*_a: Any, **_kw: Any) -> None:
        raise Exception("DB connection lost while writing breadcrumb")

    with (
        patch.object(mod, "SessionLocal", return_value=db),
        patch.object(
            mod, "run_crossload", side_effect=RuntimeError("schema drift on tradein side")
        ),
        patch.object(mod, "_finish_run", side_effect=_boom_finish),
    ):
        try:
            mod.etl_newbuilding_crossload.run(triggered_by="beat")
            raised_type = None
        except Exception as e:  # тест проверяет именно тип/сообщение исходного исключения
            raised_type = type(e)
            raised_msg = str(e)

    assert raised_type is RuntimeError
    assert "schema drift on tradein side" in raised_msg
