"""Unit tests for EnduroRussiaParser (ET-009).

Coverage:
- UT-ER-01: _parse_gpx success on valid GPX fixture
- UT-ER-02: _parse_gpx returns None on empty GPX
- UT-ER-03: bbox filter rejects out-of-bbox track
- UT-ER-04: MAPPING translates categories correctly
- UT-ER-05: base_url without dash preserved (regression R-4)
- UT-ER-06: pagination stops when fetched_so_far >= total
- UT-ER-07: HTTP 429 on /api/tracks → graceful return
- UT-ER-08: HTTP 429 on /api/tracks/{id}/gpx → graceful return, earlier tracks preserved
"""
import json
import os
from typing import Callable

import httpx

from src.api.gps_tracks.sources.enduro_russia import (
    EnduroRussiaParser,
    _bbox_intersects,
    _parse_gpx,
)
from src.api.gps_tracks.sources import enduro_russia as er_module

FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "..", "fixtures", "gps-tracks")

# Region bbox for ЦФО+Чувашия
BBOX_TSFO = (29.0, 49.5, 47.5, 60.0)


def _read_fixture(name: str) -> bytes:
    with open(os.path.join(FIXTURES_DIR, name), "rb") as f:
        return f.read()


def _make_config(**overrides) -> dict:
    cfg = {
        "id": "enduro_russia",
        "base_url": "https://endurorussia.ru",
        "rate_limit_sec": 0,  # speed up tests
        "user_agent": "test-agent",
        "source_priority": 80,
    }
    cfg.update(overrides)
    return cfg


def _patch_client(monkeypatch, handler: Callable[[httpx.Request], httpx.Response]) -> None:
    """Подменяет httpx.AsyncClient в модуле enduro_russia на клиент с MockTransport."""
    transport = httpx.MockTransport(handler)
    original = httpx.AsyncClient

    def factory(*args, **kwargs):
        kwargs["transport"] = transport
        return original(*args, **kwargs)

    monkeypatch.setattr(er_module.httpx, "AsyncClient", factory)


async def _collect_all(parser, bbox):
    """Собирает все треки из async-генератора."""
    tracks = []
    async for t in parser.collect(bbox, {}):
        tracks.append(t)
    return tracks


# ─── UT-ER-01 ───────────────────────────────────────────────────────────────

def test_ut_er_01_parse_gpx_track1_success():
    """UT-ER-01: _parse_gpx на track-1 → TrackInsert с points_count ≥ 10."""
    content = _read_fixture("enduro-russia-track-1.gpx")
    meta = {"name": "Маршрут Дмитровский", "difficulty": "hard", "created_at": "2024-08-15 12:30:00"}

    track = _parse_gpx(
        content,
        track_id=1,
        meta=meta,
        source_id="enduro_russia",
        base_url="https://endurorussia.ru",
        source_priority=80,
        mapping=EnduroRussiaParser.MAPPING,
    )

    assert track is not None
    assert track.points_count >= 10
    assert track.length_m > 0
    assert track.min_lon < track.max_lon
    assert track.min_lat < track.max_lat
    assert track.external_url == "https://endurorussia.ru/tracks/1"
    assert track.source_id == "enduro_russia"
    # difficulty 'hard' → enduro
    assert track.activity_type == "enduro"


# ─── UT-ER-02 ───────────────────────────────────────────────────────────────

def test_ut_er_02_parse_gpx_empty_returns_none():
    """UT-ER-02: _parse_gpx на пустом GPX → None."""
    content = _read_fixture("enduro-russia-track-2.gpx")

    track = _parse_gpx(
        content,
        track_id=2,
        meta={},
        source_id="enduro_russia",
        base_url="https://endurorussia.ru",
        source_priority=80,
        mapping=EnduroRussiaParser.MAPPING,
    )

    assert track is None


# ─── UT-ER-03 ───────────────────────────────────────────────────────────────

def test_ut_er_03_bbox_filter_rejects_outside():
    """UT-ER-03: track-3 за пределами bbox ЦФО → _bbox_intersects False."""
    content = _read_fixture("enduro-russia-track-3.gpx")
    track = _parse_gpx(
        content,
        track_id=3,
        meta={},
        source_id="enduro_russia",
        base_url="https://endurorussia.ru",
        source_priority=80,
        mapping=EnduroRussiaParser.MAPPING,
    )
    assert track is not None  # парсится, но bbox не пересекается
    intersects = _bbox_intersects(
        (track.min_lon, track.min_lat, track.max_lon, track.max_lat),
        BBOX_TSFO,
    )
    assert intersects is False


async def test_ut_er_03_collect_skips_out_of_bbox(monkeypatch):
    """UT-ER-03 (collect): out-of-bbox трек не yield-ится."""
    api_data = json.loads(_read_fixture("enduro-russia-api-tracks-page1.json"))
    # Оставим только один трек id=3 (вне bbox)
    api_data = {"items": [it for it in api_data["items"] if it["id"] == 3], "total": 1, "page": 0}

    def handler(req: httpx.Request) -> httpx.Response:
        if req.url.path == "/api/tracks":
            return httpx.Response(200, json=api_data)
        if req.url.path == "/api/tracks/3/gpx":
            return httpx.Response(200, content=_read_fixture("enduro-russia-track-3.gpx"))
        return httpx.Response(404)

    _patch_client(monkeypatch, handler)
    parser = EnduroRussiaParser(_make_config())
    tracks = await _collect_all(parser, BBOX_TSFO)
    assert tracks == []


# ─── UT-ER-04 ───────────────────────────────────────────────────────────────

def test_ut_er_04_mapping_categories():
    """UT-ER-04: MAPPING маппит ключевые категории."""
    m = EnduroRussiaParser.MAPPING
    assert m["hard"] == "enduro"
    assert m["soft"] == "enduro"
    assert m["мото"] == "moto"
    # 'unknown' нет в MAPPING → map_activity → 'other'
    parser = EnduroRussiaParser(_make_config())
    assert parser.map_activity("unknown") == "other"


# ─── UT-ER-05 ───────────────────────────────────────────────────────────────

def test_ut_er_05_base_url_no_dash_preserved():
    """UT-ER-05: base_url 'https://endurorussia.ru' сохраняется без замены."""
    cfg = _make_config(base_url="https://endurorussia.ru")
    parser = EnduroRussiaParser(cfg)
    assert parser.config["base_url"] == "https://endurorussia.ru"
    # Регрессия: проверим что в default fallback тоже без дефиса
    parser_no_url = EnduroRussiaParser({"id": "enduro_russia"})
    # default используется в collect() — но base_url берётся через get()
    assert "enduro-russia" not in parser_no_url.config.get("base_url", "https://endurorussia.ru")


async def test_ut_er_05_collect_uses_no_dash_url(monkeypatch):
    """UT-ER-05 (collect): HTTP-запросы уходят на endurorussia.ru (без дефиса)."""
    seen_hosts = []

    def handler(req: httpx.Request) -> httpx.Response:
        seen_hosts.append(req.url.host)
        if req.url.path == "/api/tracks":
            return httpx.Response(200, json={"items": [], "total": 0, "page": 0})
        return httpx.Response(404)

    _patch_client(monkeypatch, handler)
    parser = EnduroRussiaParser(_make_config(base_url="https://endurorussia.ru"))
    await _collect_all(parser, BBOX_TSFO)
    assert any(h == "endurorussia.ru" for h in seen_hosts)
    assert not any("enduro-russia" in h for h in seen_hosts)


# ─── UT-ER-06 ───────────────────────────────────────────────────────────────

async def test_ut_er_06_pagination_stops_at_total(monkeypatch):
    """UT-ER-06: collect() делает 1 запрос /api/tracks при total=5, items=5."""
    api_data = json.loads(_read_fixture("enduro-russia-api-tracks-page1.json"))
    list_calls = []

    def handler(req: httpx.Request) -> httpx.Response:
        if req.url.path == "/api/tracks":
            list_calls.append(req.url.query.decode() if isinstance(req.url.query, bytes) else str(req.url.query))
            return httpx.Response(200, json=api_data)
        # GPX: вернём пустой (None) или валидный для track-1
        return httpx.Response(200, content=_read_fixture("enduro-russia-track-2.gpx"))

    _patch_client(monkeypatch, handler)
    parser = EnduroRussiaParser(_make_config())
    await _collect_all(parser, BBOX_TSFO)
    assert len(list_calls) == 1, f"expected 1 /api/tracks call, got {len(list_calls)}: {list_calls}"


# ─── UT-ER-07 ───────────────────────────────────────────────────────────────

async def test_ut_er_07_http_429_on_tracks_list_graceful(monkeypatch):
    """UT-ER-07: 429 на /api/tracks → завершение без exception, 0 треков."""

    def handler(req: httpx.Request) -> httpx.Response:
        if req.url.path == "/api/tracks":
            return httpx.Response(429, json={"error": "Too Many Requests"})
        return httpx.Response(404)

    _patch_client(monkeypatch, handler)
    parser = EnduroRussiaParser(_make_config())
    tracks = await _collect_all(parser, BBOX_TSFO)
    assert tracks == []


# ─── UT-ER-08 ───────────────────────────────────────────────────────────────

async def test_ut_er_08_http_429_on_gpx_graceful(monkeypatch):
    """UT-ER-08: 429 на /api/tracks/{id}/gpx после первых OK → ранние треки сохраняются."""
    # Соберём API ответ с двумя треками: 1 (OK) и 2 (429)
    api_data = {
        "items": [
            {"id": 1, "name": "T1", "difficulty": "hard", "created_at": "2024-08-15 12:30:00"},
            {"id": 2, "name": "T2", "difficulty": "hard", "created_at": "2024-08-15 13:00:00"},
        ],
        "total": 2,
        "page": 0,
    }

    def handler(req: httpx.Request) -> httpx.Response:
        if req.url.path == "/api/tracks":
            return httpx.Response(200, json=api_data)
        if req.url.path == "/api/tracks/1/gpx":
            return httpx.Response(200, content=_read_fixture("enduro-russia-track-1.gpx"))
        if req.url.path == "/api/tracks/2/gpx":
            return httpx.Response(429)
        return httpx.Response(404)

    _patch_client(monkeypatch, handler)
    parser = EnduroRussiaParser(_make_config())
    tracks = await _collect_all(parser, BBOX_TSFO)
    # Ранний трек должен сохраниться
    assert len(tracks) == 1
    assert tracks[0].external_url.endswith("/tracks/1")
