"""Unit-тесты для ET-011 GPX-builder (`src/api/gps_tracks/export.py`).

Покрывает test-plan: UT-01, UT-02, UT-03, UT-05.
"""
from __future__ import annotations

import os
import xml.etree.ElementTree as ET

import pytest
from lxml import etree as lxml_et

from src.api.gps_tracks.export import build_gpx


GPX_NS = "http://www.topografix.com/GPX/1/1"
GPX = "{%s}" % GPX_NS


_FIXTURES_DIR = os.path.join(
    os.path.dirname(__file__), "..", "fixtures", "gpx-1.1"
)
_GPX_XSD_PATH = os.path.abspath(os.path.join(_FIXTURES_DIR, "gpx.xsd"))


@pytest.fixture(scope="module")
def gpx_schema() -> lxml_et.XMLSchema:
    """Загружает GPX 1.1 XSD-схему (см. tests/fixtures/gpx-1.1/gpx.xsd)."""
    if not os.path.exists(_GPX_XSD_PATH):
        pytest.skip(f"GPX XSD not found at {_GPX_XSD_PATH}")
    return lxml_et.XMLSchema(lxml_et.parse(_GPX_XSD_PATH))


def _validate_gpx(xml_str: str, schema: lxml_et.XMLSchema) -> None:
    """Валидирует GPX-строку по schema; падает с диагностикой при ошибке."""
    doc = lxml_et.fromstring(xml_str.encode("utf-8"))
    schema.assertValid(doc)


# ─── UT-01: корректная структура GPX 1.1 ──────────────────────────────────

def test_ut01_build_gpx_basic_structure():
    """UT-01: 5 точек, name/description/external_urls — все элементы на месте."""
    xml_str = build_gpx(
        track_id=1,
        name="Test trail",
        description="A short description",
        activity_type="enduro",
        user="testuser",
        created_at="2024-05-12T10:00:00Z",
        sources=["osm"],
        external_urls=["https://www.openstreetmap.org/way/1"],
        coords=[
            (37.60, 55.74),
            (37.61, 55.75),
            (37.62, 55.76),
            (37.63, 55.77),
            (37.64, 55.78),
        ],
    )

    # ET-парсинг — используем тот же ElementTree namespace
    root = ET.fromstring(xml_str)
    assert root.tag == f"{GPX}gpx"
    assert root.attrib["version"] == "1.1"
    assert root.attrib["creator"] == "Enduro Trails"

    metadata = root.find(f"{GPX}metadata")
    assert metadata is not None

    name_el = metadata.find(f"{GPX}name")
    assert name_el is not None and name_el.text == "Test trail"

    link_el = metadata.find(f"{GPX}link")
    assert link_el is not None
    assert link_el.attrib["href"] == "https://www.openstreetmap.org/way/1"

    trks = root.findall(f"{GPX}trk")
    assert len(trks) == 1
    trk = trks[0]
    segs = trk.findall(f"{GPX}trkseg")
    assert len(segs) == 1

    pts = segs[0].findall(f"{GPX}trkpt")
    assert len(pts) == 5
    for pt in pts:
        # lat/lon — float-парсебельные
        lat = float(pt.attrib["lat"])
        lon = float(pt.attrib["lon"])
        assert -90 <= lat <= 90
        assert -180 <= lon <= 180


def test_ut01_metadata_link_text_includes_source():
    """UT-01: text <link> = 'Источник: <source_id>'."""
    xml_str = build_gpx(
        track_id=1,
        name="x",
        description=None,
        activity_type=None,
        user=None,
        created_at=None,
        sources=["osm"],
        external_urls=["https://www.openstreetmap.org/way/42"],
        coords=[(0.0, 0.0), (1.0, 1.0)],
    )
    root = ET.fromstring(xml_str)
    link = root.find(f"{GPX}metadata/{GPX}link")
    text_el = link.find(f"{GPX}text")
    assert text_el is not None
    assert text_el.text == "Источник: osm"


def test_ut01_osm_copyright_present():
    """UT-01 / AC-10: для OSM-источника присутствует <copyright> с OSM license."""
    xml_str = build_gpx(
        track_id=1,
        name="osm track",
        description=None,
        activity_type=None,
        user=None,
        created_at=None,
        sources=["osm"],
        external_urls=["https://www.openstreetmap.org/way/123"],
        coords=[(0.0, 0.0), (1.0, 1.0)],
    )
    root = ET.fromstring(xml_str)
    cr = root.find(f"{GPX}metadata/{GPX}copyright")
    assert cr is not None
    assert cr.attrib["author"] == "Enduro Trails"
    lic = cr.find(f"{GPX}license")
    assert lic is not None
    assert lic.text == "https://www.openstreetmap.org/copyright"


# ─── UT-02: пустые / NULL поля ────────────────────────────────────────────

def test_ut02_empty_fields_no_elements():
    """UT-02: <desc>, <time>, <author>, <link> отсутствуют, а не пустые."""
    xml_str = build_gpx(
        track_id=99,
        name=None,
        description=None,
        activity_type=None,
        user=None,
        created_at=None,
        sources=[],
        external_urls=[],
        coords=[(0.0, 0.0), (1.0, 1.0)],
    )
    root = ET.fromstring(xml_str)
    metadata = root.find(f"{GPX}metadata")
    assert metadata.find(f"{GPX}desc") is None
    assert metadata.find(f"{GPX}time") is None
    assert metadata.find(f"{GPX}author") is None
    assert metadata.find(f"{GPX}link") is None
    assert metadata.find(f"{GPX}copyright") is None

    name_el = metadata.find(f"{GPX}name")
    assert name_el is not None
    assert name_el.text == "Без названия"


def test_ut02_empty_name_in_trk_too():
    xml_str = build_gpx(
        track_id=99,
        name="",
        description="",
        activity_type="",
        user="",
        created_at=None,
        sources=[],
        external_urls=[],
        coords=[(0.0, 0.0), (1.0, 1.0)],
    )
    root = ET.fromstring(xml_str)
    trk_name = root.find(f"{GPX}trk/{GPX}name")
    assert trk_name.text == "Без названия"
    # type отсутствует, потому что activity_type пустой
    assert root.find(f"{GPX}trk/{GPX}type") is None


# ─── UT-03: соответствие XSD-схеме ───────────────────────────────────────

def test_ut03_xsd_minimal(gpx_schema):
    """UT-03: минимальный трек — без metadata-полей и без activity_type."""
    xml_str = build_gpx(
        track_id=1,
        name=None,
        description=None,
        activity_type=None,
        user=None,
        created_at=None,
        sources=[],
        external_urls=[],
        coords=[(37.0, 55.0), (37.1, 55.1)],
    )
    _validate_gpx(xml_str, gpx_schema)


def test_ut03_xsd_typical(gpx_schema):
    """UT-03: типичный OSM-трек со всеми полями."""
    xml_str = build_gpx(
        track_id=10,
        name="OSM trail in Moscow",
        description="A nice trail",
        activity_type="enduro",
        user="alice",
        created_at="2024-05-12T10:00:00Z",
        sources=["osm"],
        external_urls=["https://www.openstreetmap.org/way/1"],
        coords=[(37.6 + i / 100, 55.7 + i / 100) for i in range(20)],
    )
    _validate_gpx(xml_str, gpx_schema)


def test_ut03_xsd_utf8_name(gpx_schema):
    """UT-03: UTF-8 имя/описание не ломают XSD-валидацию."""
    xml_str = build_gpx(
        track_id=42,
        name="По грязи к Чёрному озеру",
        description="Тестовое описание с & < > символами",
        activity_type="enduro",
        user="ivan",
        created_at="2025-06-01T12:34:56+03:00",
        sources=["osm", "enduro_russia"],
        external_urls=[
            "https://www.openstreetmap.org/way/9",
            "https://endurorussia.ru/tracks/9",
        ],
        coords=[(37.6, 55.7), (37.7, 55.8), (37.8, 55.9)],
    )
    _validate_gpx(xml_str, gpx_schema)


# ─── UT-05: smoke for wkb_to_coords boundary (2 точки) ──────────────────

def test_ut05_two_point_coords():
    """UT-05: минимальный LineString (2 точки) собирается корректно."""
    xml_str = build_gpx(
        track_id=1,
        name="two-pt",
        description=None,
        activity_type=None,
        user=None,
        created_at=None,
        sources=[],
        external_urls=[],
        coords=[(0.0, 0.0), (1.0, 1.0)],
    )
    root = ET.fromstring(xml_str)
    pts = root.findall(f"{GPX}trk/{GPX}trkseg/{GPX}trkpt")
    assert len(pts) == 2


# ─── Дополнительные проверки структуры ──────────────────────────────────

def test_xml_declaration_present():
    xml_str = build_gpx(
        track_id=1,
        name="x",
        description=None,
        activity_type=None,
        user=None,
        created_at=None,
        sources=[],
        external_urls=[],
        coords=[(0.0, 0.0), (1.0, 1.0)],
    )
    assert xml_str.startswith("<?xml")


def test_trkpt_coordinate_precision_6_digits():
    """ADR-014 §G: lat/lon с фиксированной точностью 6 знаков."""
    xml_str = build_gpx(
        track_id=1,
        name="x",
        description=None,
        activity_type=None,
        user=None,
        created_at=None,
        sources=[],
        external_urls=[],
        coords=[(37.123456789, 55.987654321)],
    )
    root = ET.fromstring(xml_str)
    pt = root.find(f"{GPX}trk/{GPX}trkseg/{GPX}trkpt")
    # 6 знаков после точки
    assert pt.attrib["lon"] == "37.123457"
    assert pt.attrib["lat"] == "55.987654"


def test_non_osm_source_no_osm_copyright():
    """ADR-014 §G: для не-OSM источников нет OSM-license в <copyright>."""
    xml_str = build_gpx(
        track_id=1,
        name="wikiloc-only",
        description=None,
        activity_type=None,
        user=None,
        created_at=None,
        sources=["wikiloc"],
        external_urls=["https://www.wikiloc.com/x"],
        coords=[(0.0, 0.0), (1.0, 1.0)],
    )
    root = ET.fromstring(xml_str)
    cr = root.find(f"{GPX}metadata/{GPX}copyright")
    # либо <copyright> отсутствует, либо license != OSM URL
    if cr is not None:
        lic = cr.find(f"{GPX}license")
        assert lic is None or lic.text != "https://www.openstreetmap.org/copyright"


def test_time_normalized_to_utc():
    """ADR-014 §G: <metadata><time> приводится к UTC с суффиксом Z."""
    xml_str = build_gpx(
        track_id=1,
        name="x",
        description=None,
        activity_type=None,
        user=None,
        created_at="2024-05-12T13:00:00+03:00",
        sources=[],
        external_urls=[],
        coords=[(0.0, 0.0), (1.0, 1.0)],
    )
    root = ET.fromstring(xml_str)
    time_el = root.find(f"{GPX}metadata/{GPX}time")
    assert time_el is not None
    # +03:00 → UTC = 10:00:00Z
    assert time_el.text == "2024-05-12T10:00:00Z"
