"""
Integration tests для ET-001: блокировка шлагбаумов и исключение тротуаров.

Тесты делятся на две группы:

1. Статические проверки lua-профиля (TC-004, частично AC-1/AC-2).
   Запускаются всегда — анализируют исходник `infra/osrm/enduro.lua`.

2. Интеграционные проверки через работающий OSRM (TC-001, TC-002, TC-003, TC-005).
   Требуют поднятого OSRM с пересобранным графом. Если OSRM недоступен —
   соответствующие тесты помечаются `skip`, чтобы CI без инфраструктуры не падал.

Адрес OSRM берётся из переменной окружения OSRM_URL (default: http://172.22.0.1:5559).
Координаты тестовых точек задаются переменными окружения с осмысленными default'ами
для региона ЦФО — `architect` может уточнить их позже.
"""

from __future__ import annotations

import os
import re
import subprocess
from pathlib import Path
from shutil import which

import httpx
import pytest

REPO_ROOT = Path(__file__).resolve().parents[2]
LUA_PROFILE_PATH = REPO_ROOT / "infra" / "osrm" / "enduro.lua"

OSRM_URL = os.environ.get("OSRM_URL", "http://172.22.0.1:5559")
OSRM_TIMEOUT = float(os.environ.get("OSRM_TIMEOUT", "10"))

# Тестовые координаты (lon, lat). Architect может переопределить через env.
# Default'ы — заведомо валидные точки в ЦФО (Москва и окрестности).
BARRIER_FROM = (
    float(os.environ.get("TEST_BARRIER_FROM_LON", "37.6173")),
    float(os.environ.get("TEST_BARRIER_FROM_LAT", "55.7558")),
)
BARRIER_TO = (
    float(os.environ.get("TEST_BARRIER_TO_LON", "37.6500")),
    float(os.environ.get("TEST_BARRIER_TO_LAT", "55.7600")),
)
# Узел заведомо известного шлагбаума (lon, lat). Если не задан — тест проверяет
# только сам факт получения 200/404 без падений.
BARRIER_NODE = (
    float(os.environ.get("TEST_BARRIER_NODE_LON", "0")) or None,
    float(os.environ.get("TEST_BARRIER_NODE_LAT", "0")) or None,
)

CITY_FROM = (
    float(os.environ.get("TEST_CITY_FROM_LON", "37.6173")),
    float(os.environ.get("TEST_CITY_FROM_LAT", "55.7558")),
)
CITY_TO = (
    float(os.environ.get("TEST_CITY_TO_LON", "37.6300")),
    float(os.environ.get("TEST_CITY_TO_LAT", "55.7600")),
)

CATTLE_GRID_FROM = (
    float(os.environ.get("TEST_CG_FROM_LON", "37.6173")),
    float(os.environ.get("TEST_CG_FROM_LAT", "55.7558")),
)
CATTLE_GRID_TO = (
    float(os.environ.get("TEST_CG_TO_LON", "37.7000")),
    float(os.environ.get("TEST_CG_TO_LAT", "55.8000")),
)

REGRESSION_FROM = (
    float(os.environ.get("TEST_REG_FROM_LON", "37.6173")),
    float(os.environ.get("TEST_REG_FROM_LAT", "55.7558")),
)
REGRESSION_TO = (
    float(os.environ.get("TEST_REG_TO_LON", "37.8000")),
    float(os.environ.get("TEST_REG_TO_LAT", "55.9000")),
)

# Если шлагбаум попал в маршрут — координата ноды должна оказаться достаточно
# близко к одной из точек геометрии. Порог в градусах (~5 м на широте 55°).
COORD_NEAR_DEG = 5e-5


def _osrm_available() -> bool:
    """Проверяет что OSRM отвечает на /health (или хотя бы на корень)."""
    try:
        with httpx.Client(timeout=2.0) as client:
            resp = client.get(f"{OSRM_URL}/health")
            if resp.status_code < 500:
                return True
    except Exception:
        pass
    try:
        with httpx.Client(timeout=2.0) as client:
            # Любой ответ от роутера сойдёт — нам нужно убедиться что порт жив.
            resp = client.get(f"{OSRM_URL}/route/v1/driving/0,0;1,1")
            return resp.status_code < 500
    except Exception:
        return False


osrm_required = pytest.mark.skipif(
    not _osrm_available(),
    reason=f"OSRM не доступен на {OSRM_URL} — интеграционный тест пропущен",
)


def _route(from_lonlat: tuple[float, float], to_lonlat: tuple[float, float]) -> httpx.Response:
    """Делает запрос к OSRM и возвращает ответ."""
    coords = f"{from_lonlat[0]},{from_lonlat[1]};{to_lonlat[0]},{to_lonlat[1]}"
    url = (
        f"{OSRM_URL}/route/v1/driving/{coords}"
        f"?overview=full&geometries=geojson&alternatives=false&radiuses=5000;5000"
    )
    with httpx.Client(timeout=OSRM_TIMEOUT) as client:
        return client.get(url)


# ──────────────────────────────────────────────────────────────────────────────
# Статические проверки lua-профиля (без OSRM)
# ──────────────────────────────────────────────────────────────────────────────

def _read_profile() -> str:
    assert LUA_PROFILE_PATH.is_file(), f"Lua-профиль не найден: {LUA_PROFILE_PATH}"
    return LUA_PROFILE_PATH.read_text(encoding="utf-8")


def test_lua_syntax():
    """TC-004: lua-профиль синтаксически корректен.

    Если в системе есть `luac` — используем его. Иначе ограничиваемся
    структурными проверками: файл читается, содержит ключевые функции и
    сбалансированные `function`/`end`.
    """
    profile = _read_profile()

    # Структурные инварианты — должны быть всегда.
    assert "function process_node" in profile, "нет process_node"
    assert "function process_way" in profile, "нет process_way"
    assert "function process_turn" in profile, "нет process_turn"
    assert "function setup" in profile, "нет setup"
    assert "return {" in profile, "нет финального экспорта"

    luac = which("luac") or which("luac5.3") or which("luac5.1")
    if luac:
        result = subprocess.run(
            [luac, "-p", str(LUA_PROFILE_PATH)],
            capture_output=True,
            text=True,
        )
        assert result.returncode == 0, (
            f"luac -p вернул {result.returncode}: {result.stderr}"
        )


def test_blocked_barriers_match_trz():
    """AC-1: профиль блокирует ровно перечисленный в ТЗ набор barrier-типов."""
    profile = _read_profile()
    expected = {
        "gate", "bollard", "lift_gate", "chain",
        "cycle_barrier", "motorcycle_barrier", "border_control", "block",
    }
    # Парсим таблицу blocked_barriers
    match = re.search(r"local\s+blocked_barriers\s*=\s*\{([^}]*)\}", profile, re.S)
    assert match, "не нашли таблицу blocked_barriers"
    found = set(re.findall(r"(\w+)\s*=\s*true", match.group(1)))
    assert found == expected, (
        f"blocked_barriers содержит {found}, ожидали {expected}"
    )

    # cattle_grid и ford не должны быть в блоке
    assert "cattle_grid" not in found, "cattle_grid не должен блокироваться"
    assert "ford" not in found, "ford не должен блокироваться"

    # mode.inaccessible должен использоваться в process_node
    node_match = re.search(
        r"function\s+process_node.*?\nend",
        profile,
        re.S,
    )
    assert node_match, "не нашли тело process_node"
    body = node_match.group(0)
    assert "mode.inaccessible" in body, (
        "process_node должен использовать mode.inaccessible (ADR-001)"
    )
    assert "forward_mode" in body and "backward_mode" in body, (
        "process_node должен выставлять и forward_mode, и backward_mode"
    )


def test_excluded_highways_match_trz():
    """AC-2: footway/pedestrian/steps/corridor исключены из графа."""
    profile = _read_profile()
    expected = {"footway", "pedestrian", "steps", "corridor"}
    match = re.search(r"local\s+excluded_highways\s*=\s*\{([^}]*)\}", profile, re.S)
    assert match, "не нашли таблицу excluded_highways"
    found = set(re.findall(r"(\w+)\s*=\s*true", match.group(1)))
    assert found == expected, (
        f"excluded_highways содержит {found}, ожидали {expected}"
    )

    # И ни одного из них не должно остаться в highway_rate.
    rate_match = re.search(r"local\s+highway_rate\s*=\s*\{([^}]*)\}", profile, re.S)
    assert rate_match, "не нашли таблицу highway_rate"
    rate_body = rate_match.group(1)
    for hw in expected:
        # Проверяем именно ключ таблицы — пробелы между ключом и `=` допустимы.
        assert not re.search(rf"\b{hw}\s*=", rate_body), (
            f"{hw} остался в highway_rate — должен быть удалён (AC-2)"
        )


# ──────────────────────────────────────────────────────────────────────────────
# Интеграционные тесты через OSRM
# ──────────────────────────────────────────────────────────────────────────────

@osrm_required
def test_route_avoids_barrier():
    """TC-001: маршрут через точку с известным шлагбаумом обходит её.

    Если переменные TEST_BARRIER_NODE_* не заданы — тест ограничивается
    проверкой того, что OSRM либо возвращает валидный маршрут, либо честно
    отвечает NoRoute (404), без internal-ошибок.
    """
    resp = _route(BARRIER_FROM, BARRIER_TO)
    assert resp.status_code < 500, f"OSRM 5xx: {resp.status_code} {resp.text[:200]}"

    data = resp.json()
    assert data.get("code") in {"Ok", "NoRoute", "NoSegment"}, (
        f"неожиданный OSRM-код: {data.get('code')}"
    )

    if data.get("code") != "Ok":
        # Заблокированный участок может приводить к NoRoute — это ожидаемое
        # поведение (см. ADR-001, раздел «Последствия»).
        return

    routes = data.get("routes") or []
    assert routes, "OSRM=Ok, но routes пуст"
    coords = routes[0]["geometry"]["coordinates"]
    assert len(coords) >= 2, "геометрия маршрута слишком короткая"

    node_lon, node_lat = BARRIER_NODE
    if node_lon is None or node_lat is None:
        # Координата ноды-шлагбаума не задана — детальная проверка невозможна,
        # но сам факт корректного ответа — уже AC-1 на уровне smoke.
        return

    for lon, lat in coords:
        assert not (
            abs(lon - node_lon) < COORD_NEAR_DEG
            and abs(lat - node_lat) < COORD_NEAR_DEG
        ), (
            f"маршрут проходит через заблокированный шлагбаум "
            f"({node_lon}, {node_lat})"
        )


@osrm_required
def test_route_no_footway():
    """TC-002: маршрут в городе не использует footway/pedestrian/steps.

    Проверяем через OSRM-annotations: если в ответе есть classes/nodes
    с пешеходным типом — тест падает. Без annotations ограничиваемся проверкой
    того, что маршрут построен и не пустой.
    """
    coords = f"{CITY_FROM[0]},{CITY_FROM[1]};{CITY_TO[0]},{CITY_TO[1]}"
    url = (
        f"{OSRM_URL}/route/v1/driving/{coords}"
        f"?overview=full&geometries=geojson&alternatives=false"
        f"&annotations=true&steps=true&radiuses=5000;5000"
    )
    with httpx.Client(timeout=OSRM_TIMEOUT) as client:
        resp = client.get(url)

    assert resp.status_code < 500, f"OSRM 5xx: {resp.status_code}"
    data = resp.json()
    if data.get("code") != "Ok":
        # В городе может не найтись маршрута если все варианты — тротуары.
        # Это и есть желаемое поведение (AC-2): пешеходные дороги выкинуты.
        assert data.get("code") in {"NoRoute", "NoSegment"}, (
            f"неожиданный код: {data.get('code')}"
        )
        return

    routes = data.get("routes") or []
    assert routes, "OSRM=Ok, но routes пуст"
    route = routes[0]
    assert route.get("distance", 0) > 0, "distance должен быть > 0"

    # Если OSRM вернул steps — заглянем в name/ref/maneuver на наличие
    # явно пешеходных подсказок (грубый, но дешёвый sanity check).
    legs = route.get("legs", [])
    for leg in legs:
        for step in leg.get("steps", []):
            name = (step.get("name") or "").lower()
            assert "тротуар" not in name, "step с тротуаром в name"
            assert "footway" not in name, "step с footway в name"


@osrm_required
def test_route_allows_cattle_grid():
    """TC-003: cattle_grid не блокирует маршрут (мотоцикл проезжает)."""
    resp = _route(CATTLE_GRID_FROM, CATTLE_GRID_TO)
    assert resp.status_code < 500, f"OSRM 5xx: {resp.status_code}"
    data = resp.json()
    # Главное — что cattle_grid не превращает маршрут в NoRoute.
    # Допускаем NoSegment если тестовая точка не привязалась к графу.
    assert data.get("code") in {"Ok", "NoSegment"}, (
        f"cattle_grid не должен ломать маршрут, но получили {data.get('code')}"
    )
    if data.get("code") == "Ok":
        routes = data.get("routes") or []
        assert routes, "OSRM=Ok, но routes пуст"
        assert routes[0].get("distance", 0) > 0


@osrm_required
def test_existing_route_works():
    """TC-005 (регрессия): обычный маршрут без шлагбаумов/тротуаров строится."""
    resp = _route(REGRESSION_FROM, REGRESSION_TO)
    assert resp.status_code == 200, f"OSRM статус: {resp.status_code}"
    data = resp.json()
    assert data.get("code") == "Ok", f"OSRM код: {data.get('code')}"
    routes = data.get("routes") or []
    assert routes, "routes пуст"
    route = routes[0]
    assert route.get("distance", 0) > 0, "distance должен быть > 0"
    geom = route.get("geometry", {})
    assert geom.get("type") == "LineString"
    assert len(geom.get("coordinates", [])) >= 2, "геометрия пуста"
