"""Invariants over data/sql migrations (issue #2216).

Прод применяет миграции по BARE FILENAME: deploy-tradein.yml трекает каждый
`data/sql/*.sql` в таблице `public._schema_migrations` (PRIMARY KEY = filename).
Из этого следуют два хрупких инварианта, которые этот тест защищает от регрессии:

1. Переименование / удаление УЖЕ ПРИМЕНЁННОЙ миграции ломает прод: новый
   filename считается неприменённым и прогоняется повторно (дубль-INSERT,
   повторный DDL, PK violation под ON_ERROR_STOP => деплой падает или, хуже,
   молча дублирует данные). Manifest `data/sql/_manifest_applied.txt` — это
   слепок применённых имён на момент коммита; любой из них ОБЯЗАН существовать.

2. Два разных файла с одинаковым NN-префиксом ("дубль-префикс") — источник
   двусмысленного порядка применения (`ls | sort` детерминирован, но человек
   легко создаёт коллизию). 6 исторических дублей grandfathered'ы (оба в
   manifest). Любой НОВЫЙ файл обязан нести уникальный префикс.

Self-maintenance: добавляя новую миграцию, допиши её имя в manifest В ТОМ ЖЕ PR
(см. assert-сообщения ниже). Тест требует data/sql ⊇ manifest и уникальность
префикса у новых файлов; сам manifest дополняет автор миграции.
"""

from __future__ import annotations

import re
from pathlib import Path

_BACKEND_ROOT = Path(__file__).resolve().parents[1]
_SQL_DIR = _BACKEND_ROOT / "data" / "sql"
_MANIFEST = _SQL_DIR / "_manifest_applied.txt"

_NN_PREFIX = re.compile(r"^(\d+)_")


def _read_manifest() -> list[str]:
    """Имена миграций из manifest; # comments и пустые строки игнорируются."""
    names: list[str] = []
    for raw in _MANIFEST.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        names.append(line)
    return names


def _actual_sql_files() -> set[str]:
    return {p.name for p in _SQL_DIR.glob("*.sql")}


def _prefix(name: str) -> str | None:
    m = _NN_PREFIX.match(name)
    return m.group(1) if m else None


def test_manifest_entries_all_exist() -> None:
    """Каждый файл из manifest СУЩЕСТВУЕТ в data/sql/.

    Red => применённая миграция переименована или удалена. Прод трекает по
    bare-filename: старое имя остаётся в _schema_migrations, НОВОЕ имя считается
    неприменённым и прогоняется повторно. Восстанови исходное имя файла (или,
    если переименование намеренное — так делать НЕЛЬЗЯ для уже-применённых
    миграций: заведи НОВЫЙ файл, а старый оставь как есть).
    """
    actual = _actual_sql_files()
    manifest = _read_manifest()
    missing = sorted(n for n in manifest if n not in actual)
    assert not missing, (
        "Миграции из _manifest_applied.txt отсутствуют в data/sql/ "
        f"(переименованы/удалены?): {missing}. Эти имена уже применены на проде "
        "(tracking по bare-filename в _schema_migrations) — их нельзя "
        "переименовывать/удалять. Верни исходные имена файлов."
    )


def test_manifest_is_sorted_and_unique() -> None:
    """Manifest отсортирован (codepoint) и без дублей — детерминированный слепок."""
    manifest = _read_manifest()
    assert manifest == sorted(manifest), (
        "_manifest_applied.txt не отсортирован. Пересортируй записи "
        "(LC_ALL=C sort / Python sorted())."
    )
    dupes = sorted({n for n in manifest if manifest.count(n) > 1})
    assert not dupes, f"Дублирующиеся строки в _manifest_applied.txt: {dupes}"


def test_new_files_do_not_reuse_prefix() -> None:
    """Новые (не в manifest) .sql-файлы НЕ переиспользуют существующий NN-префикс.

    Grandfathered дубли (084/108/113/121/124/130) — оба файла в manifest, поэтому
    не флагаются: считаются "существующими", а не "новыми".

    Red => новый файл взял префикс уже присутствующей миграции. Присвой
    следующий свободный NN и допиши имя в _manifest_applied.txt (тот же PR).
    """
    actual = _actual_sql_files()
    manifest = set(_read_manifest())

    # Префиксы, «занятые» уже-применёнными (manifest) миграциями.
    baseline_prefixes: set[str] = set()
    for name in manifest:
        p = _prefix(name)
        if p is not None:
            baseline_prefixes.add(p)

    new_files = sorted(actual - manifest)
    collisions: list[str] = []
    # Внутри новых файлов префикс тоже обязан быть уникален (два новых с одним NN).
    seen_new_prefix: dict[str, str] = {}
    for name in new_files:
        p = _prefix(name)
        if p is None:
            continue
        if p in baseline_prefixes:
            collisions.append(f"{name} (префикс {p} занят применённой миграцией)")
        elif p in seen_new_prefix:
            collisions.append(f"{name} (префикс {p} уже у нового {seen_new_prefix[p]})")
        else:
            seen_new_prefix[p] = name

    assert not collisions, (
        "Новые миграции переиспользуют NN-префикс: " + "; ".join(collisions) + ". "
        "Присвой следующий свободный номер и добавь имя файла в "
        "_manifest_applied.txt в ЭТОМ ЖЕ PR."
    )


def test_manifest_covers_all_but_new_files() -> None:
    """data/sql ⊇ manifest, и каждый новый файл имеет уникальный префикс —
    напоминание о self-maintenance: manifest дополняется вместе с миграцией.

    Этот тест НЕ требует, чтобы новый файл уже был в manifest (иначе PR с новой
    миграцией всегда красный). Он лишь гарантирует, что manifest не отстал от
    реальности В ЧАСТИ применённых имён (см. test_manifest_entries_all_exist) и
    что новые файлы не создают префикс-коллизий (см. предыдущий тест).
    """
    # Sanity: manifest непустой и в data/sql есть файлы — защита от битых путей.
    assert _actual_sql_files(), f"Не найдено *.sql в {_SQL_DIR}"
    assert _read_manifest(), f"_manifest_applied.txt пуст: {_MANIFEST}"
