"""ET-006 — тесты загрузки и визуализации GPX-треков.

Изменение ET-006 — фронтендовое: новый модуль `src/web/gpx.js` плюс
правки `src/web/index.html`, `src/web/app.css` и одна строка-хук в
`src/web/app.js` (см. `06-adr/ADR-002`, `ADR-003`).

В CI исполняется только `pytest tests/`, поэтому файл покрывает фичу
двумя способами (по аналогии с `tests/unit/test_unit_toggle.py`):

1. Статические проверки структуры `gpx.js`, `index.html`, `app.css`,
   `app.js` — выполняются всегда, без внешних зависимостей.
2. Поведенческие JS unit-тесты (группы U-01..U-21 из
   `04-test-plan.yaml`) — через встроенный тест-раннер Node
   (`node --test`). Если `node` отсутствует — часть помечается `skip`.

E2E-сценарии (E-01..E-10) требуют Playwright-инфраструктуры, которой в
репозитории нет, — они остаются за этапом тестирования.
"""

from __future__ import annotations

import subprocess
from pathlib import Path
from shutil import which

import pytest

REPO_ROOT = Path(__file__).resolve().parents[2]
INDEX_HTML = REPO_ROOT / "src" / "web" / "index.html"
APP_JS = REPO_ROOT / "src" / "web" / "app.js"
GPX_JS = REPO_ROOT / "src" / "web" / "gpx.js"
APP_CSS = REPO_ROOT / "src" / "web" / "app.css"
JS_TEST = REPO_ROOT / "tests" / "unit" / "gpx.test.js"


def _read(path: Path) -> str:
    assert path.is_file(), f"не найден {path}"
    return path.read_text(encoding="utf-8")


# ──────────────────────────────────────────────────────────────────────────────
# Статические проверки gpx.js (ADR-002, ADR-003, 08-data-requirements.md)
# ──────────────────────────────────────────────────────────────────────────────

def test_gpx_module_exists():
    """ADR-002: GPX-фича вынесена в отдельный модуль src/web/gpx.js."""
    assert GPX_JS.is_file(), "не найден src/web/gpx.js"


def test_gpx_module_public_api():
    """ADR-002: модуль определяет публичный контракт парсинга и расчётов."""
    js = _read(GPX_JS)
    for fn in (
        "parseGpxText", "parseGpxAsync", "extractGpxModel",
        "trackStats", "colorForIndex", "haversineKm",
        "tracksToGeoJSON", "waypointsToGeoJSON", "fileBounds",
    ):
        assert f"function {fn}(" in js, f"в gpx.js не определена функция {fn}()"


def test_gpx_module_exports_for_browser_and_node():
    """ADR-002: window.Gpx для браузера и module.exports для unit-тестов."""
    js = _read(GPX_JS)
    assert "global.Gpx = Gpx" in js, "gpx.js не публикует глобальный неймспейс Gpx"
    assert "module.exports" in js, "gpx.js не экспортируется для Node unit-тестов"


def test_gpx_module_publishes_onclick_handlers():
    """ADR-002: модуль публикует обработчики inline-onclick и хук REQ-F-13."""
    js = _read(GPX_JS)
    for fn in (
        "onGpxFileSelected", "toggleGpxSheet",
        "selectGpxTrack", "removeGpxTrack", "rebuildGpxOverlays",
    ):
        assert f"global.{fn} =" in js, f"gpx.js не публикует глобаль {fn}"


def test_gpx_palette_eight_colors():
    """TRZ REQ-F-04 §5.3: палитра треков — ровно 8 цветов."""
    js = _read(GPX_JS)
    for color in (
        "#e6194b", "#3cb44b", "#ffe119", "#4363d8",
        "#f58231", "#911eb4", "#42d4f4", "#f032e6",
    ):
        assert color in js, f"в палитре GPX нет цвета {color}"


def test_gpx_file_size_limit():
    """TRZ REQ-F-03: лимит размера GPX-файла — 50 МБ."""
    js = _read(GPX_JS)
    assert "50 * 1024 * 1024" in js, "не задан лимит размера файла 50 МБ"


def test_gpx_noise_filter_constant():
    """TRZ §5.2: дельты высот < 2 м фильтруются как GPS-шум."""
    js = _read(GPX_JS)
    assert "ELE_NOISE_M" in js, "нет константы фильтрации шума высот"


def test_gpx_uses_domparser_main_thread():
    """ADR-003: парсинг XML — через DOMParser в основном потоке."""
    js = _read(GPX_JS)
    assert "new DOMParser()" in js, "gpx.js не использует DOMParser (ADR-003)"
    assert "Worker" not in js, "ADR-003: Web Worker не используется"


def test_gpx_chunked_conversion():
    """ADR-003: конвертация DOM → модель выполняется чанками."""
    js = _read(GPX_JS)
    assert "CHUNK_SIZE" in js, "нет чанковой конвертации (ADR-003)"


# ──────────────────────────────────────────────────────────────────────────────
# Статические проверки index.html (TRZ §3, REQ-F-01, REQ-F-09)
# ──────────────────────────────────────────────────────────────────────────────

def test_gpx_upload_button_present():
    """TRZ REQ-F-01 §3.1: кнопка загрузки GPX в правой панели карты."""
    html = _read(INDEX_HTML)
    assert 'id="btn-gpx-upload"' in html, "нет кнопки загрузки GPX btn-gpx-upload"
    assert 'id="gpx-file-input"' in html, "нет input[type=file] для GPX"
    assert 'accept=".gpx"' in html, "input не ограничен расширением .gpx"
    assert "multiple" in html, "TRZ REQ-F-01: допускается множественный выбор"
    assert 'onchange="onGpxFileSelected(this)"' in html, "input не привязан к обработчику"


def test_gpx_upload_button_between_compass_and_locate():
    """TRZ §3.1: кнопка GPX расположена между «Компас» и «Геолокация»."""
    html = _read(INDEX_HTML)
    compass = html.index('id="btn-compass"')
    gpx = html.index('id="btn-gpx-upload"')
    locate = html.index('onclick="locateMe()"')
    assert compass < gpx < locate, "кнопка GPX должна быть между компасом и геолокацией"


def test_gpx_toolbar_button_present():
    """TRZ §3.2: кнопка «GPX» в нижнем тулбаре переключает sheet."""
    html = _read(INDEX_HTML)
    assert 'id="tb-gpx"' in html, "нет кнопки GPX в тулбаре"
    assert 'onclick="toggleGpxSheet()"' in html, "кнопка тулбара не вызывает toggleGpxSheet"


def test_gpx_sheet_present():
    """TRZ REQ-F-09 §3.3: bottom sheet #sheet-gpx со списком и деталями."""
    html = _read(INDEX_HTML)
    assert 'id="sheet-gpx"' in html, "нет панели #sheet-gpx"
    for el in ("gpx-list", "gpx-stats", "gpx-elevation-canvas", "gpx-empty"):
        assert f'id="{el}"' in html, f"в #sheet-gpx нет элемента {el}"


def test_gpx_sheet_uses_bottom_sheet_component():
    """TRZ REQ-F-09: панель GPX переиспользует компонент .bottom-sheet."""
    html = _read(INDEX_HTML)
    start = html.index('id="sheet-gpx"')
    container_start = html.rfind("<div", 0, start)
    container_open_end = html.index(">", container_start)
    assert "bottom-sheet" in html[container_start:container_open_end], (
        "панель GPX должна использовать класс bottom-sheet"
    )


def test_gpx_toast_and_loading_present():
    """TRZ §3.4, REQ-NF-01: toast-уведомления и индикатор парсинга."""
    html = _read(INDEX_HTML)
    assert 'id="app-toast"' in html, "нет контейнера toast-уведомлений"
    assert 'id="gpx-loading"' in html, "нет индикатора загрузки GPX"


def test_gpx_js_loaded_after_app_js():
    """ADR-002: gpx.js подключается строго ПОСЛЕ app.js."""
    html = _read(INDEX_HTML)
    gpx_pos = html.find('src="gpx.js"')
    app_pos = html.find('src="app.js"')
    assert gpx_pos != -1, "gpx.js не подключён в index.html"
    assert app_pos != -1, "app.js не подключён в index.html"
    assert gpx_pos > app_pos, "gpx.js должен подключаться ПОСЛЕ app.js (ADR-002)"


# ──────────────────────────────────────────────────────────────────────────────
# Статические проверки app.js (ADR-002 — контракт интеграции, REQ-F-13)
# ──────────────────────────────────────────────────────────────────────────────

def test_app_js_has_rebuild_gpx_hook():
    """ADR-002 / REQ-F-13: rebuildMapOverlays() восстанавливает GPX-слои."""
    js = _read(APP_JS)
    assert "rebuildGpxOverlays" in js, "в app.js нет хука rebuildGpxOverlays"
    # Хук защищён typeof — app.js остаётся валидным и без gpx.js (ADR-002).
    assert "typeof rebuildGpxOverlays === 'function'" in js, (
        "хук GPX должен быть защищён проверкой typeof (ADR-002)"
    )


def test_app_js_gpx_hook_inside_rebuild_overlays():
    """ADR-002: хук GPX размещён внутри функции rebuildMapOverlays()."""
    js = _read(APP_JS)
    start = js.index("function rebuildMapOverlays(")
    end = js.index("\n}", start)
    assert "rebuildGpxOverlays" in js[start:end], (
        "хук rebuildGpxOverlays должен вызываться из rebuildMapOverlays()"
    )


# ──────────────────────────────────────────────────────────────────────────────
# Статические проверки app.css (TRZ §3.3, §3.4)
# ──────────────────────────────────────────────────────────────────────────────

def test_gpx_styles_present():
    """TRZ §3: для панели, статистики и профиля заданы стили в app.css."""
    css = _read(APP_CSS)
    for selector in (
        "#app-toast", "#gpx-loading", ".gpx-row",
        ".gpx-stats-grid", "#gpx-elevation-canvas",
    ):
        assert selector in css, f"в app.css нет стилей для {selector}"


# ──────────────────────────────────────────────────────────────────────────────
# Поведенческие JS unit-тесты через Node (U-01..U-21)
# ──────────────────────────────────────────────────────────────────────────────

node_required = pytest.mark.skipif(
    which("node") is None,
    reason="node не установлен — поведенческие JS unit-тесты пропущены",
)


@node_required
def test_js_unit_tests_pass():
    """U-01..U-21: запускает behavioral JS-тесты gpx.js через `node --test`."""
    assert JS_TEST.is_file(), f"не найден JS-тест {JS_TEST}"
    node = which("node")
    result = subprocess.run(
        [node, "--test", str(JS_TEST)],
        capture_output=True,
        text=True,
        cwd=str(REPO_ROOT),
    )
    assert result.returncode == 0, (
        f"JS unit-тесты GPX упали (код {result.returncode}):\n"
        f"{result.stdout}\n{result.stderr}"
    )
