"""Static guards for migration 165 (issue #2204 — полное выключение источника n1).

Прод применяет data/sql построчно строго (ON_ERROR_STOP). Полный DB-прогон
требует живой БД; здесь фиксируем структурные инварианты, которые ГАРАНТИРУЮТ
идемпотентность и НЕдеструктивность к самим listings-строкам по построению:

  1. Транзакционность (BEGIN/COMMIT) — атомарный апдейт.
  2. НЕдеструктивность к данным: строки listings source='n1' НЕ удаляются, только
     деактивируются (is_active=false) — исторические объявления остаются в БД.
     Никакого `DELETE FROM listings` в коде.
  3. Идемпотентность по построению:
       - страховочный UPDATE ограничен `AND is_active` -> повторный прогон no-op;
       - `DELETE ... WHERE source IN (...)` / `WHERE source = 'n1'` -> повторный
         прогон удаляет 0 строк.
  4. Скоуп чистки конфига: обе n1-строки расписаний (n1_city_sweep +
     deactivate_stale_n1) и per-source knob scraper_settings 'n1'.

Комментарий-заголовок в прозе объясняет намерение (в т.ч. фразу «Никакого
DELETE FROM listings»), поэтому destructive-проверки смотрят на ИСПОЛНЯЕМЫЙ SQL
(`--`-комменты вырезаны), а не на прозу.
"""

from __future__ import annotations

import re
from pathlib import Path

_SQL_DIR = Path(__file__).resolve().parents[1] / "data" / "sql"
_MIGRATION_165 = _SQL_DIR / "165_remove_n1_source.sql"


def _sql() -> str:
    return _MIGRATION_165.read_text(encoding="utf-8")


def _executable_sql() -> str:
    """SQL без комментариев — только исполняемые строки (`--`-комменты вырезаны).

    В этой миграции `--` не встречается внутри строковых литералов, поэтому
    split по первому `--` безопасно отделяет код от построчного комментария.
    """
    lines = []
    for raw in _sql().splitlines():
        code = raw.split("--", 1)[0]
        if code.strip():
            lines.append(code)
    return "\n".join(lines)


def _flat(text: str) -> str:
    return re.sub(r"\s+", " ", text).strip().lower()


def test_migration_165_exists() -> None:
    assert _MIGRATION_165.exists(), f"missing migration: {_MIGRATION_165}"


def test_migration_165_is_transactional() -> None:
    sql = _sql()
    assert "BEGIN;" in sql
    assert "COMMIT;" in sql


def test_migration_165_does_not_delete_listings() -> None:
    """Строки listings source='n1' НЕ удаляются — только деактивируются."""
    flat = _flat(_executable_sql())
    assert "delete from listings" not in flat
    # Деактивация вместо удаления.
    assert "update listings" in flat
    assert "set is_active = false" in flat


def test_migration_165_listings_update_idempotent_and_scoped() -> None:
    """UPDATE ограничен source='n1' AND is_active -> повторный прогон no-op."""
    flat = _flat(_executable_sql())
    assert "where source = 'n1'" in flat
    assert "and is_active" in flat


def test_migration_165_deletes_both_n1_schedules() -> None:
    flat = _flat(_executable_sql())
    assert "delete from scrape_schedules" in flat
    assert "'n1_city_sweep'" in flat
    assert "'deactivate_stale_n1'" in flat


def test_migration_165_deletes_n1_scraper_settings() -> None:
    flat = _flat(_executable_sql())
    assert "delete from scraper_settings" in flat
    assert "where source = 'n1'" in flat


def test_migration_165_no_psycopg_trap() -> None:
    """Никаких :param::type — psycopg v3 требует CAST(... AS type)."""
    assert not re.search(r":\w+::", _sql())
