"""Тесты schema-фильтрованного чтения latest_run_for (#3b-i, repository.py).

latest_run_for получил keyword-only ``schema_version``:
  • None (дефолт): читает из v_analysis_runs_latest (последний ран ЛЮБОЙ схемы) —
    backward-compatible, существующие вызовы не меняются.
  • задан: читает из БАЗОВОЙ таблицы analysis_runs с фильтром schema_version —
    последний ран ИМЕННО этой схемы (нужно форсайт-таске достать 'analyze-1.0' базу
    даже когда поверх легли §22-раны '1.0', которые вернул бы view).

Чистые тесты (mock session, без живой БД): проверяем форму SQL (какая таблица /
фильтр / ORDER BY) и что фильтрованный режим прокидывает schema_version в params и
возвращает именно отфильтрованную строку. psycopg v3: CAST(:x AS text), не :x::type.
"""

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 ANALYZE_SCHEMA_VERSION, latest_run_for


def _mock_db_capturing() -> tuple[MagicMock, list[tuple[Any, Any]]]:
    """Mock session: захватывает (stmt, params) каждого execute, .first() → sentinel-row.

    Возвращает (db, captured) — captured[i] = (stmt, params) i-го execute. Сам Row
    мокаем как MagicMock (тесту достаточно, что вернулся именно он).
    """
    captured: list[tuple[Any, Any]] = []
    sentinel_row = MagicMock(name="row")

    def execute_router(stmt: Any, params: Any = None) -> MagicMock:
        captured.append((stmt, params))
        result = MagicMock()
        result.first.return_value = sentinel_row
        return result

    db = MagicMock()
    db.execute = execute_router
    db._sentinel_row = sentinel_row  # для assert identity
    return db, captured


def test_latest_run_for_default_uses_view_no_schema_filter() -> None:
    """Дефолт (schema_version=None): читает v_analysis_runs_latest, без фильтра схемы."""
    db, captured = _mock_db_capturing()

    row = latest_run_for(db, "66:41:0000000:1")

    assert row is db._sentinel_row
    assert len(captured) == 1
    stmt, params = captured[0]
    upper = str(stmt).upper()
    assert "FROM V_ANALYSIS_RUNS_LATEST" in upper
    assert "SCHEMA_VERSION =" not in upper  # дефолт НЕ фильтрует по схеме
    assert "CAST(:CAD_NUM AS TEXT)" in upper
    assert params == {"cad_num": "66:41:0000000:1"}


def test_latest_run_for_schema_filtered_uses_base_table() -> None:
    """schema_version задан: читает БАЗОВУЮ analysis_runs с фильтром + ORDER BY DESC LIMIT 1
    и прокидывает schema_version в params."""
    db, captured = _mock_db_capturing()

    row = latest_run_for(db, "66:41:0000000:1", schema_version=ANALYZE_SCHEMA_VERSION)

    assert row is db._sentinel_row
    assert len(captured) == 1
    stmt, params = captured[0]
    upper = str(stmt).upper()
    flat = " ".join(upper.split())
    # базовая таблица, НЕ view
    assert "FROM ANALYSIS_RUNS" in upper
    assert "V_ANALYSIS_RUNS_LATEST" not in upper
    # фильтр по схеме + latest-by-created_at
    assert "SCHEMA_VERSION = CAST(:SCHEMA_VERSION AS TEXT)" in upper
    assert "ORDER BY CREATED_AT DESC LIMIT 1" in flat
    assert "CAST(:CAD_NUM AS TEXT)" in upper
    # оба bind прокинуты
    assert params == {
        "cad_num": "66:41:0000000:1",
        "schema_version": ANALYZE_SCHEMA_VERSION,
    }


def test_latest_run_for_no_double_colon_cast() -> None:
    """psycopg v3 (оба режима): ни один bind не использует :name::type (recurring bug)."""
    db, captured = _mock_db_capturing()
    latest_run_for(db, "x")
    latest_run_for(db, "x", schema_version=ANALYZE_SCHEMA_VERSION)
    for stmt, _ in captured:
        sql = str(stmt)
        assert not re.search(r":[a-z_]+::[a-z]", sql), f"found :x::type cast in: {sql}"


def test_latest_run_for_schema_filter_returns_filtered_not_newer_row() -> None:
    """Поведенческий: при наличии более НОВОГО '1.0' рана фильтрованный режим должен
    вернуть последний 'analyze-1.0', а НЕ '1.0' (модель «view вернул бы §22, мы — base»).

    Мокаем БД так, что фильтрованный запрос (по 'analyze-1.0') отдаёт analyze-строку,
    а запрос без фильтра (view, latest-overall) отдал бы более новую §22-строку. Так
    закрепляем смысл фичи: для базы форсайта берём именно analyze-ран, не свежий §22.
    """
    analyze_row = MagicMock(name="analyze-1.0-row")
    analyze_row.schema_version = "analyze-1.0"
    forecast_row = MagicMock(name="1.0-row")
    forecast_row.schema_version = "1.0"

    def execute_router(stmt: Any, params: Any = None) -> MagicMock:
        upper = str(stmt).upper()
        result = MagicMock()
        if "FROM ANALYSIS_RUNS" in upper and "SCHEMA_VERSION =" in upper:
            # фильтрованный запрос: вернёт строку запрошенной схемы
            wanted = (params or {}).get("schema_version")
            result.first.return_value = analyze_row if wanted == "analyze-1.0" else forecast_row
        else:
            # view (без фильтра) — latest-overall = более новый §22-ран
            result.first.return_value = forecast_row
        return result

    db = MagicMock()
    db.execute = execute_router

    # фильтрованный режим → analyze-ран (не свежий §22)
    got = latest_run_for(db, "66:41:0000000:1", schema_version="analyze-1.0")
    assert got is analyze_row
    assert got.schema_version == "analyze-1.0"

    # дефолт (view) → latest-overall = §22-ран (контраст, доказывает зачем фильтр)
    got_view = latest_run_for(db, "66:41:0000000:1")
    assert got_view is forecast_row
