"""ET-005 — тесты переключения единиц измерения расстояний (км/мили).

Изменение ET-005 — исключительно фронтендовое: новый модуль
`src/web/units.js` плюс правки `src/web/index.html`, `src/web/app.js`,
`src/web/app.css` (см. `06-adr/adr-0001-unit-toggle-client-side.md`).
В CI исполняется только `pytest tests/`, поэтому файл покрывает фичу
двумя способами:

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

Браузерный e2e-сценарий TP-05 (mobile responsive) требует Playwright-
инфраструктуры, которой в репозитории нет; его поведенческая суть в
доступном объёме покрыта статическими проверками UI ниже.
"""

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"
UNITS_JS = REPO_ROOT / "src" / "web" / "units.js"
APP_CSS = REPO_ROOT / "src" / "web" / "app.css"
JS_TEST = REPO_ROOT / "tests" / "unit" / "units.test.js"


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


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

def test_units_module_exists():
    """ADR-0001 п.2: модуль размещён в src/web/units.js (не static/js/)."""
    assert UNITS_JS.is_file(), "не найден src/web/units.js"


def test_units_module_public_api():
    """ADR-0001 п.3: модуль определяет публичный контракт Units."""
    js = _read(UNITS_JS)
    for fn in ("getUnit", "setUnit", "toggleUnit", "formatDistance"):
        assert f"function {fn}(" in js, f"в units.js не определена функция {fn}()"


def test_units_module_constants():
    """AC-2 / 08-data-requirements.md §5: коэффициент и ключ хранилища."""
    js = _read(UNITS_JS)
    assert "0.621371" in js, "в units.js нет коэффициента перевода 1 км = 0.621371 ми"
    assert "'distance_unit'" in js, "ключ localStorage distance_unit не объявлен"


def test_units_module_exports_for_browser_and_node():
    """ADR-0001 п.3: window.Units для браузера и module.exports для тестов."""
    js = _read(UNITS_JS)
    assert "global.Units" in js, "units.js не публикует глобальный неймспейс Units"
    assert "module.exports" in js, "units.js не экспортируется для Node unit-тестов"


# ──────────────────────────────────────────────────────────────────────────────
# Статические проверки index.html (AC-1, риск R7)
# ──────────────────────────────────────────────────────────────────────────────

def test_unit_toggle_present_in_html():
    """AC-1: сегментированный переключатель км/мили присутствует в попапе."""
    html = _read(INDEX_HTML)
    assert 'id="unit-seg"' in html, "нет переключателя единиц unit-seg"
    assert 'id="unit-btn-km"' in html, "нет кнопки «км»"
    assert 'id="unit-btn-mi"' in html, "нет кнопки «мили»"
    assert "onclick=\"onUnitToggle('km')\"" in html, "кнопка «км» не привязана к onUnitToggle"
    assert "onclick=\"onUnitToggle('mi')\"" in html, "кнопка «мили» не привязана к onUnitToggle"


def test_unit_toggle_reuses_seg_control_component():
    """ADR-0001 п.8: переиспользуется готовый компонент .seg-control."""
    html = _read(INDEX_HTML)
    start = html.index('id="unit-seg"')
    container_start = html.rfind("<div", 0, start)
    container_open_end = html.index(">", container_start)
    assert "seg-control" in html[container_start:container_open_end], (
        "переключатель единиц должен использовать класс seg-control"
    )


def test_units_js_loaded_before_app_js():
    """Риск R7: units.js подключается строго перед app.js."""
    html = _read(INDEX_HTML)
    units_pos = html.find('src="units.js"')
    app_pos = html.find('src="app.js"')
    assert units_pos != -1, "units.js не подключён в index.html"
    assert app_pos != -1, "app.js не подключён в index.html"
    assert units_pos < app_pos, "units.js должен подключаться ДО app.js (риск R7)"


def test_unit_toggle_has_styles():
    """AC-4: для строки переключателя единиц заданы стили в app.css."""
    css = _read(APP_CSS)
    assert ".terrain-unit-row" in css, "нет стилей строки переключателя единиц"


# ──────────────────────────────────────────────────────────────────────────────
# Статические проверки app.js (ADR-0001 п.6-7, риски R1, R3, R6)
# ──────────────────────────────────────────────────────────────────────────────

def test_app_js_unit_functions_defined():
    """ADR-0001 п.6: определены UI-обработчик и оркестратор пересчёта."""
    js = _read(APP_JS)
    for fn in ("onUnitToggle", "syncUnitToggleUI", "onUnitChange"):
        assert f"function {fn}(" in js, f"не определена функция {fn}()"


def test_app_js_has_et005_block_markers():
    """Блок ET-005 обрамлён маркерами (как POI-блок ET-002)."""
    js = _read(APP_JS)
    assert "// >>> ET-005 unit toggle block >>>" in js, "нет открывающего маркера блока ET-005"
    assert "// <<< ET-005 unit toggle block <<<" in js, "нет закрывающего маркера блока ET-005"


def test_app_js_single_unitchange_subscription():
    """ADR-0001 п.6: ровно одна подписка-оркестратор на событие unitchange."""
    js = _read(APP_JS)
    assert js.count("addEventListener('unitchange'") == 1, (
        "подписка на unitchange должна быть единственной (оркестратор)"
    )
    assert "addEventListener('unitchange', onUnitChange)" in js, (
        "событие unitchange не привязано к оркестратору onUnitChange"
    )


def test_app_js_uses_centralized_formatter():
    """Риск R1: места форматирования расстояний переведены на units.js."""
    js = _read(APP_JS)
    # 13 call-sites из 10-tech-risks.md R1 минус GPX (R6) → ≥ 11 вызовов.
    assert js.count("Units.formatDistance(") >= 11, (
        "не все места форматирования расстояний переведены на Units.formatDistance()"
    )


def test_app_js_distance_helpers_delegate_to_units():
    """Риск R1/R4: единые форматтеры делегируют расчёт модулю units.js."""
    js = _read(APP_JS)
    for fn in ("formatDist", "formatSegmentDist"):
        idx = js.index(f"function {fn}(")
        body = js[idx:idx + 220]
        assert "Units.formatDistance(" in body, (
            f"{fn}() должен делегировать форматирование в units.js"
        )


def test_app_js_scale_bar_is_unit_aware():
    """Риск R3: масштабная линейка учитывает выбранную единицу."""
    js = _read(APP_JS)
    assert "Units.getUnit() === 'mi'" in js, "scale-bar не реагирует на режим миль (R3)"
    assert "window._updateScaleZoom" in js, (
        "updateScaleZoom не доступен оркестратору onUnitChange()"
    )


def test_app_js_gpx_export_stays_metric():
    """Риск R6: экспорт GPX не конвертируется в мили (остаётся метрическим)."""
    js = _read(APP_JS)
    gpx_start = js.index("function generateGPX(")
    gpx_body = js[gpx_start:gpx_start + 1400]
    assert "Units.formatDistance(" not in gpx_body, (
        "generateGPX() не должен конвертировать расстояния (риск R6)"
    )


def test_app_js_restores_unit_choice_on_load():
    """AC-3: выбор единицы восстанавливается при инициализации страницы."""
    js = _read(APP_JS)
    assert "syncUnitToggleUI()" in js, "состояние переключателя не восстанавливается при загрузке"


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

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


@node_required
def test_js_unit_tests_pass():
    """TP-01..TP-04: запускает behavioral JS-тесты units.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-тесты единиц измерения упали (код {result.returncode}):\n"
        f"{result.stdout}\n{result.stderr}"
    )
