"""Тесты read-функций истории ранов (#994, repository.py): list_runs_for / get_run.

Чистые тесты (mock session, без живой БД): проверяем форму SQL —
  • list_runs_for: LIGHT-проекция (НЕ тянет result), БАЗОВАЯ таблица (не view),
    ORDER BY created_at DESC, LIMIT через CAST(:limit AS integer), bind cad_num/limit;
    пустой список при .all() == [].
  • get_run: full-проекция (включая result), фильтр по id через CAST(:run_id AS integer),
    None при .first() == None.
  • psycopg v3: ни один bind не использует :name::type (recurring bug class, backend.md).
"""

from __future__ import annotations

import os
import re
from typing import Any
from unittest.mock import MagicMock

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

from app.services.analysis_runs.repository import get_run, list_runs_for


def _mock_db_capturing(
    *, all_rows: list[Any] | None = None, first_row: Any | None = None
) -> tuple[MagicMock, list[tuple[Any, Any]]]:
    """Mock session: захватывает (stmt, params); .all()→all_rows, .first()→first_row."""
    captured: list[tuple[Any, Any]] = []

    def execute_router(stmt: Any, params: Any = None) -> MagicMock:
        captured.append((stmt, params))
        result = MagicMock()
        result.all.return_value = all_rows if all_rows is not None else []
        result.first.return_value = first_row
        return result

    db = MagicMock()
    db.execute = execute_router
    return db, captured


# ── list_runs_for ─────────────────────────────────────────────────────────────


def test_list_runs_for_light_projection_base_table_order_limit() -> None:
    """LIGHT-проекция без result; базовая таблица; ORDER BY created_at DESC; LIMIT cast."""
    rows = [MagicMock(name="r1"), MagicMock(name="r2")]
    db, captured = _mock_db_capturing(all_rows=rows)

    got = list_runs_for(db, "66:41:0000000:1", limit=20)

    assert got == rows
    assert len(captured) == 1
    stmt, params = captured[0]
    upper = str(stmt).upper()
    flat = " ".join(upper.split())
    # базовая таблица, НЕ view (view вернул бы максимум 1 строку на cad)
    assert "FROM ANALYSIS_RUNS" in upper
    assert "V_ANALYSIS_RUNS_LATEST" not in upper
    # LIGHT: result-блоб НЕ выбирается
    assert "RESULT" not in upper, "list_runs_for НЕ должен тянуть тяжёлый result"
    # выбраны именно метаданные
    for col in ("ID", "CAD_NUM", "CREATED_AT", "STATUS", "SCHEMA_VERSION",
                "DISTRICT", "CONFIDENCE", "CREATED_BY"):
        assert col in upper, f"ожидали колонку {col} в LIGHT-проекции"
    assert "ORDER BY CREATED_AT DESC" in flat
    assert "LIMIT CAST(:LIMIT AS INTEGER)" in upper
    assert "CAST(:CAD_NUM AS TEXT)" in upper
    assert params == {"cad_num": "66:41:0000000:1", "limit": 20}


def test_list_runs_for_empty_returns_empty_list() -> None:
    """Нет ранов (.all() == []) → пустой список (caller отдаст 200 + {runs: []})."""
    db, _ = _mock_db_capturing(all_rows=[])
    assert list_runs_for(db, "66:00:0000000:0") == []


def test_list_runs_for_default_limit_is_20() -> None:
    """limit по умолчанию = 20 (kwarg-only)."""
    db, captured = _mock_db_capturing(all_rows=[])
    list_runs_for(db, "x")
    _, params = captured[0]
    assert params["limit"] == 20


# ── get_run ───────────────────────────────────────────────────────────────────


def test_get_run_full_projection_includes_result_filters_by_id() -> None:
    """Full-проекция (с result); фильтр id через CAST(:run_id AS integer)."""
    sentinel = MagicMock(name="full-row")
    db, captured = _mock_db_capturing(first_row=sentinel)

    got = get_run(db, 123)

    assert got is sentinel
    assert len(captured) == 1
    stmt, params = captured[0]
    upper = str(stmt).upper()
    assert "FROM ANALYSIS_RUNS" in upper
    assert "RESULT" in upper, "get_run должен включать result-блоб"
    assert "WHERE ID = CAST(:RUN_ID AS INTEGER)" in " ".join(upper.split())
    assert params == {"run_id": 123}


def test_get_run_missing_returns_none() -> None:
    """Рана нет (.first() == None) → None (caller → 404 graceful)."""
    db, _ = _mock_db_capturing(first_row=None)
    assert get_run(db, 999) is None


# ── psycopg v3 guard ──────────────────────────────────────────────────────────


def test_run_history_no_double_colon_cast() -> None:
    """Ни один bind не использует :name::type (psycopg v3, backend.md)."""
    db, captured = _mock_db_capturing(all_rows=[], first_row=None)
    list_runs_for(db, "x")
    get_run(db, 1)
    for stmt, _ in captured:
        sql = str(stmt)
        assert not re.search(r":[a-z_]+::[a-z]", sql), f"found :x::type cast in: {sql}"
