"""Статические тесты healthcheck-конфигурации (ET-015).

Покрывает критерии приёмки:
    AC-03  → ST-01  — в healthcheck нет `curl`.
    AC-04  → ST-02  — Dockerfile не ставит `curl`/`wget` через apt-get.
    AC-06  → ST-03  — healthcheck использует python + stdlib (urllib, sys).
    AC-07  → ST-04  — внутренний `timeout=N` < внешнего YAML-`timeout`.
    AC-09  → ST-06  — CHANGELOG содержит запись с упоминанием ET-015.
    AC-10  → ST-07  — ADR по решению существует в work-item.

Источник правды по конфигурации:
    docs/work-items/ET-015/02-trz.md §3.1
    docs/work-items/ET-015/06-adr/ADR-020-healthcheck-via-python-urllib.md
"""
from __future__ import annotations

import re
from pathlib import Path

import pytest
import yaml

REPO_ROOT = Path(__file__).resolve().parents[2]
COMPOSE_PATH = REPO_ROOT / "docker-compose.yml"
DOCKERFILE_PATH = REPO_ROOT / "Dockerfile"
CHANGELOG_PATH = REPO_ROOT / "CHANGELOG.md"
ADR_DIR = REPO_ROOT / "docs" / "work-items" / "ET-015" / "06-adr"


def _load_app_healthcheck() -> dict:
    data = yaml.safe_load(COMPOSE_PATH.read_text(encoding="utf-8"))
    services = data.get("services", {})
    assert "app" in services, "docker-compose.yml must define service `app`"
    hc = services["app"].get("healthcheck")
    assert hc is not None, "service `app` must define `healthcheck`"
    return hc


def _yaml_duration_to_seconds(value: str) -> float:
    """Парсит YAML-длительность вида '5s' / '500ms' / '2m' в секунды."""
    if isinstance(value, (int, float)):
        return float(value)
    s = str(value).strip()
    m = re.fullmatch(r"(\d+(?:\.\d+)?)\s*(ms|s|m|h)", s)
    assert m, f"Не могу распарсить duration {value!r}"
    n = float(m.group(1))
    unit = m.group(2)
    return {"ms": n / 1000, "s": n, "m": n * 60, "h": n * 3600}[unit]


# ───────────────────────── ST-01 (AC-03) ─────────────────────────

def test_st01_healthcheck_does_not_use_curl():
    hc = _load_app_healthcheck()
    test = hc.get("test")
    assert test is not None, "healthcheck.test обязателен"
    joined = " ".join(str(x) for x in test) if isinstance(test, list) else str(test)
    assert "curl" not in joined, (
        f"healthcheck.test содержит `curl`, ожидался python one-liner. test={test!r}"
    )


# ───────────────────────── ST-02 (AC-04) ─────────────────────────

def test_st02_dockerfile_does_not_apt_install_curl_or_wget():
    df = DOCKERFILE_PATH.read_text(encoding="utf-8")
    # Ищем "apt-get install ... curl" / "... wget" — строго по слову.
    bad_lines = [
        line
        for line in df.splitlines()
        if re.search(r"apt-get\s+install[^\n]*\b(curl|wget)\b", line)
    ]
    assert not bad_lines, (
        f"Dockerfile устанавливает curl/wget через apt-get, "
        f"что противоречит ADR-020. Найдено: {bad_lines!r}"
    )


# ───────────────────────── ST-03 (AC-06) ─────────────────────────

def test_st03_healthcheck_uses_python_and_stdlib():
    hc = _load_app_healthcheck()
    test = hc["test"]
    assert isinstance(test, list), f"healthcheck.test должен быть массивом, а не {type(test).__name__}"
    assert len(test) >= 4, f"healthcheck.test должен иметь минимум 4 элемента, есть {len(test)}"
    assert test[0] == "CMD", f"первый элемент должен быть 'CMD' (не CMD-SHELL), есть {test[0]!r}"
    assert test[1] == "python", f"второй элемент должен быть 'python', есть {test[1]!r}"
    assert test[2] == "-c", f"третий элемент должен быть '-c', есть {test[2]!r}"

    code = test[3]
    assert isinstance(code, str)
    assert "urllib.request" in code, "one-liner должен использовать urllib.request"
    assert "sys.exit" in code, "one-liner должен явно вызывать sys.exit для exit code"

    # Запрещённые сторонние пакеты — гарантируем «только stdlib».
    forbidden = ["requests", "httpx", "aiohttp", "urllib3"]
    for pkg in forbidden:
        # Ищем именно как импорт/обращение к пакету, а не подстроку.
        assert not re.search(rf"\b{re.escape(pkg)}\b", code), (
            f"one-liner ссылается на сторонний пакет {pkg!r}; должен использовать только stdlib"
        )


# ───────────────────────── ST-04 (AC-07) ─────────────────────────

def test_st04_internal_timeout_less_than_external():
    hc = _load_app_healthcheck()
    code = hc["test"][3]
    m = re.search(r"timeout\s*=\s*(\d+(?:\.\d+)?)", code)
    assert m, f"в one-liner ожидается явный аргумент timeout=N, не нашли в {code!r}"
    internal = float(m.group(1))

    external_raw = hc.get("timeout")
    assert external_raw is not None, "healthcheck.timeout должен быть задан"
    external = _yaml_duration_to_seconds(external_raw)

    assert internal < external, (
        f"внутренний timeout={internal}s должен быть СТРОГО меньше "
        f"внешнего healthcheck.timeout={external}s (TRZ §3.1, AC-07)"
    )


# ───────────────────────── ST-06 (AC-09) ─────────────────────────

def test_st06_changelog_mentions_et015():
    text = CHANGELOG_PATH.read_text(encoding="utf-8")
    # Проверяем именно строку, не просто наличие подстроки в любом месте.
    matches = [line for line in text.splitlines() if "ET-015" in line]
    assert matches, "CHANGELOG.md должен содержать запись с упоминанием ET-015 (см. TRZ R-4 / AC-09)"


# ───────────────────────── ST-07 (AC-10) ─────────────────────────

def test_st07_adr_exists():
    assert ADR_DIR.is_dir(), f"директория ADR должна существовать: {ADR_DIR}"
    md_files = sorted(ADR_DIR.glob("*.md"))
    assert md_files, f"в {ADR_DIR} должен быть хотя бы один ADR (.md)"

    # Хотя бы один файл должен описывать решение healthcheck-via-python.
    relevant = []
    for path in md_files:
        body = path.read_text(encoding="utf-8").lower()
        if "healthcheck" in body and ("urllib" in body or "python" in body):
            relevant.append(path.name)
    assert relevant, (
        f"в {ADR_DIR} нет ADR, описывающего решение healthcheck через python urllib. "
        f"Найдены файлы: {[p.name for p in md_files]}"
    )


# ───────────────────────── Дополнительная защита от регресса ─────────────────────────

def test_app_healthcheck_target_is_local_api_health():
    """one-liner должен бить именно в /api/health на localhost:5556 (TRZ §3.1)."""
    hc = _load_app_healthcheck()
    code = hc["test"][3]
    assert "http://localhost:5556/api/health" in code, (
        f"healthcheck должен обращаться к http://localhost:5556/api/health, "
        f"чтобы корректно проверять loopback контейнера. Код: {code!r}"
    )


def test_app_healthcheck_has_start_period():
    """ADR-020 добавляет start_period для смягчения окна холодного старта."""
    hc = _load_app_healthcheck()
    assert "start_period" in hc, "ADR-020 требует start_period для healthcheck (см. TRZ §3.1)"
    sp = _yaml_duration_to_seconds(hc["start_period"])
    assert sp >= 10, f"start_period слишком мал ({sp}s), ожидается ≥ 10s (TRZ §3.1)"


@pytest.mark.parametrize("field,minimum", [("interval", 30), ("retries", 3)])
def test_app_healthcheck_preserves_baseline_params(field, minimum):
    """ADR-020 «инвариант»: interval/retries не уменьшаются относительно текущих."""
    hc = _load_app_healthcheck()
    assert field in hc, f"healthcheck.{field} обязателен"
    value = hc[field]
    if field == "interval":
        value = _yaml_duration_to_seconds(value)
    assert value >= minimum, (
        f"healthcheck.{field}={value} меньше базового {minimum} — нарушает инвариант ADR-020"
    )
