"""ET-014 — тесты sheet ⇄ terrain-popup взаимодействия (ADR-019).

ET-014 — исключительно фронтендовое изменение (см. ADR-019): правки
`src/web/app.js`. Никаких изменений в CSS, HTML, backend, миграциях.
В CI исполняется только `pytest tests/`, поэтому файл покрывает фичу
двумя способами:

1. Статические проверки структуры `src/web/app.js` — выполняются всегда.
2. Поведенческие JS unit-тесты (TC-U-02, REQ-F-04, REQ-F-06) —
   запускаются через встроенный тест-раннер Node (`node --test`). Если
   `node` в системе отсутствует — эта часть помечается `skip`.

Браузерные e2e-сценарии (TC-E-01..TC-E-06, TC-UI-01..TC-UI-08) требуют
Playwright-инфраструктуры, которой в репозитории нет. Их поведенческая
суть покрыта JS unit-тестами и статическими проверками ниже.

См.:
- ADR-019: docs/work-items/ET-014/06-adr/ADR-019-terrain-popup-yields-to-sheet.md
- TRZ: docs/work-items/ET-014/02-trz.md
- AC: docs/work-items/ET-014/03-acceptance-criteria.md
- Test plan: docs/work-items/ET-014/04-test-plan.yaml
"""

from __future__ import annotations

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

import pytest

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


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


# ──────────────────────────────────────────────────────────────────────────────
# Статические проверки app.js (ADR-019)
# ──────────────────────────────────────────────────────────────────────────────

def test_app_js_has_et014_block_markers():
    """Блок ET-014 обрамлён маркерами для извлечения JS unit-тестами."""
    js = _read(APP_JS)
    assert "// >>> ET-014 sheet-popup yield block" in js, (
        "нет открывающего маркера блока ET-014"
    )
    assert "// <<< ET-014 sheet-popup yield block <<<" in js, (
        "нет закрывающего маркера блока ET-014"
    )


def test_close_terrain_popup_function_defined():
    """ADR-019 §Решение/1: функция closeTerrainPopup() определена."""
    js = _read(APP_JS)
    assert "function closeTerrainPopup(" in js, (
        "не определена функция closeTerrainPopup()"
    )


def test_close_terrain_popup_inside_block():
    """closeTerrainPopup() расположена внутри ET-014-блока (для unit-тестов)."""
    js = _read(APP_JS)
    block_start = js.index("// >>> ET-014 sheet-popup yield block")
    block_end = js.index("// <<< ET-014 sheet-popup yield block <<<")
    block = js[block_start:block_end]
    assert "function closeTerrainPopup(" in block, (
        "closeTerrainPopup() должна быть внутри ET-014-блока"
    )


def test_open_sheet_calls_close_terrain_popup_first():
    """ADR-019 §Решение/2: closeTerrainPopup() — первый вызов в openSheet()
    после null-check на sheet."""
    js = _read(APP_JS)
    # Берём тело openSheet до первой закрывающей фигурной скобки на новой строке.
    m = re.search(
        r"function openSheet\(id\)\s*\{([\s\S]*?)\n\}",
        js,
    )
    assert m, "функция openSheet(id) не найдена"
    body = m.group(1)
    # Проверим порядок: null-check, потом closeTerrainPopup, потом всё остальное.
    nullcheck_pos = body.find("if (!sheet) return;")
    close_popup_pos = body.find("closeTerrainPopup()")
    close_sheet_pos = body.find("closeSheet(")
    add_open_pos = body.find("classList.add('open')")

    assert nullcheck_pos >= 0, "null-check на sheet в openSheet() отсутствует"
    assert close_popup_pos > nullcheck_pos, (
        "closeTerrainPopup() должна вызываться ПОСЛЕ null-check"
    )
    assert close_sheet_pos > close_popup_pos, (
        "closeTerrainPopup() должна вызываться ДО закрытия других sheets"
    )
    assert add_open_pos > close_popup_pos, (
        "closeTerrainPopup() должна вызываться ДО classList.add('open')"
    )


def test_open_sheet_calls_close_terrain_popup_exactly_once():
    """REQ-NF-02: никакого дублирования вызовов (не должно быть лишних
    обработчиков). closeTerrainPopup() вызывается ровно один раз в openSheet."""
    js = _read(APP_JS)
    m = re.search(
        r"function openSheet\(id\)\s*\{([\s\S]*?)\n\}",
        js,
    )
    assert m, "функция openSheet(id) не найдена"
    body = m.group(1)
    calls = body.count("closeTerrainPopup()")
    assert calls == 1, (
        f"closeTerrainPopup() должна вызываться ровно один раз в openSheet(), "
        f"найдено {calls}"
    )


# ──────────────────────────────────────────────────────────────────────────────
# Статические проверки: что НЕ меняется (ADR-019 §Что НЕ меняется)
# ──────────────────────────────────────────────────────────────────────────────

def test_z_index_stack_unchanged_for_affected_widgets():
    """ADR-019 §Что НЕ меняется: z-index ключевых виджетов из конфликта
    (.bottom-sheet, #sheet-backdrop, .terrain-popup, #marker-dialog)
    остаётся неизменным. Эти значения — фундамент аргументации ADR-019
    (Вариант A не правит CSS), любая их правка ломает обоснование.

    REQ-NF-03: marker-dialog (z=500) сохраняется на верху относительно sheet'ов.
    """
    css = _read(APP_CSS)
    expected = [
        (".bottom-sheet", "z-index: 400"),
        ("#sheet-backdrop", "z-index: 390"),
        ("#marker-dialog", "z-index: 500"),
        (".terrain-popup", "z-index: 500"),
    ]
    for selector, z in expected:
        sel_pos = css.find(selector)
        assert sel_pos >= 0, f"селектор {selector} не найден в app.css"
        # Смотрим в окне 600 символов после селектора (CSS-блок укладывается).
        window = css[sel_pos:sel_pos + 600]
        assert z in window, (
            f"в блоке {selector} отсутствует {z}; ADR-019 запрещает менять z-stack"
        )


def test_gps_tracks_js_not_touched_by_et014():
    """ADR-019 §Что НЕ меняется: src/web/gps_tracks.js не правится ET-014."""
    gps = _read(REPO_ROOT / "src" / "web" / "gps_tracks.js")
    # Маркеров ET-014 в gps_tracks.js не должно быть — логика живёт в openSheet.
    assert "ET-014" not in gps, (
        "ET-014 не должен изменять src/web/gps_tracks.js (см. ADR-019)"
    )


def test_index_html_not_touched_by_et014():
    """ADR-019 §Что НЕ меняется: src/web/index.html без изменений."""
    html = _read(INDEX_HTML)
    assert "ET-014" not in html, (
        "ET-014 не должен изменять src/web/index.html (см. ADR-019)"
    )


# ──────────────────────────────────────────────────────────────────────────────
# Поведенческие JS unit-тесты через Node (TC-U-02, REQ-F-04, REQ-F-06)
# ──────────────────────────────────────────────────────────────────────────────

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


@node_required
def test_js_unit_tests_pass():
    """TC-U-02 / REQ-F-04 / REQ-F-06: behavioral 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-тесты ET-014 упали (код {result.returncode}):\n"
        f"{result.stdout}\n{result.stderr}"
    )
