"""test_server_resource_block.py — юниты для блокировки тяжёлых ресурсов (perf).

Проверяет:
  1. _parse_bool — общий разбор булевых env-флагов;
  2. _parse_block_types — разбор BROWSER_BLOCK_RESOURCE_TYPES CSV (дефолт font,media);
  3. _apply_resource_block навешивает route когда _BLOCKED_TYPES непуст и НЕ навешивает
     когда пуст (no-op);
  4. сам route-handler аборт'ит блокируемые типы и continue'ит document/script/xhr;
  5. сбой abort() деградирует в continue_() (fallback), не роняя навигацию.

camoufox/Playwright НЕ поднимается: route + request мокаются заглушками. Хендлер
вызывается напрямую через захваченный _apply_resource_block-callback.

Запуск (из tradein-mvp/browser/)::

    python -m pytest test_server_resource_block.py -q
"""

from __future__ import annotations

import asyncio
import importlib.util
from pathlib import Path
from typing import Any

import pytest

# server.py — не пакет (отдельный сервис без __init__/pyproject). Грузим по пути.
_SERVER_PATH = Path(__file__).resolve().parent / "server.py"
_spec = importlib.util.spec_from_file_location("tradein_browser_server", _SERVER_PATH)
assert _spec is not None and _spec.loader is not None
server = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(server)


# ── _parse_bool ──────────────────────────────────────────────────────────────────


def test_parse_bool_truthy() -> None:
    for raw in ("true", "True", "1", "yes", "on", " TRUE "):
        assert server._parse_bool(raw, False) is True, raw


def test_parse_bool_falsy() -> None:
    for raw in ("false", "0", "no", "off", "garbage", ""):
        assert server._parse_bool(raw, True) is False, raw


def test_parse_bool_none_uses_default() -> None:
    assert server._parse_bool(None, True) is True
    assert server._parse_bool(None, False) is False


def test_parse_block_types_default_font_media() -> None:
    """Дефолт BROWSER_BLOCK_RESOURCE_TYPES (env не задана) — font/media."""
    assert server._parse_block_types(None) == frozenset({"font", "media"})


def test_parse_block_types_empty_string_disables() -> None:
    """Пустая строка → пустой набор (блокировка выключена)."""
    assert server._parse_block_types("") == frozenset()


def test_parse_block_types_csv_trim_lower() -> None:
    """CSV парсится с trim/lower, дубликаты схлопываются."""
    assert server._parse_block_types(" Font , MEDIA ,font") == frozenset({"font", "media"})


# ── route-interception заглушки ──────────────────────────────────────────────────


class _FakeRequest:
    def __init__(self, resource_type: str) -> None:
        self.resource_type = resource_type


class _FakeRoute:
    """Поддельный Playwright Route: считает abort/continue, мокает request."""

    def __init__(self, resource_type: str, fail_abort: bool = False) -> None:
        self.request = _FakeRequest(resource_type)
        self.aborted = 0
        self.continued = 0
        self._fail_abort = fail_abort

    async def abort(self) -> None:
        if self._fail_abort:
            raise RuntimeError("route already handled")
        self.aborted += 1

    async def continue_(self) -> None:
        self.continued += 1


class _FakePage:
    """Поддельная page: захватывает callback из page.route(pattern, handler)."""

    def __init__(self) -> None:
        self.route_pattern: str | None = None
        self.route_handler: Any = None

    async def route(self, pattern: str, handler: Any) -> None:
        self.route_pattern = pattern
        self.route_handler = handler


def _capture_handler(monkeypatch: pytest.MonkeyPatch, *, blocked: set[str]) -> Any:
    """Ставит _BLOCKED_TYPES, прогоняет _apply_resource_block, возвращает page (с handler).

    Пустой blocked → блокировка выключена (no-op), route не навешивается.
    """
    monkeypatch.setattr(server, "_BLOCKED_TYPES", frozenset(blocked))
    page = _FakePage()
    asyncio.run(server._apply_resource_block(page))
    return page


def test_apply_resource_block_noop_when_empty(monkeypatch: pytest.MonkeyPatch) -> None:
    """Пустой _BLOCKED_TYPES (BROWSER_BLOCK_RESOURCE_TYPES="") → route НЕ навешивается."""
    page = _capture_handler(monkeypatch, blocked=set())
    assert page.route_handler is None
    assert page.route_pattern is None


def test_apply_resource_block_registers_route_when_nonempty(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Непустой набор → route на '**/*' с хендлером."""
    page = _capture_handler(monkeypatch, blocked={"font", "media"})
    assert page.route_pattern == "**/*"
    assert callable(page.route_handler)


def test_handler_aborts_blocked_types(monkeypatch: pytest.MonkeyPatch) -> None:
    """Дефолтные font/media (в _BLOCKED_TYPES) → abort, без continue."""
    page = _capture_handler(monkeypatch, blocked={"font", "media"})
    for rtype in ("font", "media"):
        route = _FakeRoute(rtype)
        asyncio.run(page.route_handler(route))
        assert route.aborted == 1, rtype
        assert route.continued == 0, rtype


def test_handler_continues_document_and_script(monkeypatch: pytest.MonkeyPatch) -> None:
    """document/script/stylesheet/xhr/fetch/image — НЕ блокируются (гидрация/fingerprint)."""
    page = _capture_handler(monkeypatch, blocked={"font", "media"})
    for rtype in ("document", "script", "stylesheet", "xhr", "fetch", "image"):
        route = _FakeRoute(rtype)
        asyncio.run(page.route_handler(route))
        assert route.aborted == 0, rtype
        assert route.continued == 1, rtype


def test_handler_falls_back_to_continue_on_abort_error(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """abort() кинул (route уже обработан) → fallback continue_(), навигация не падает."""
    page = _capture_handler(monkeypatch, blocked={"font"})
    route = _FakeRoute("font", fail_abort=True)
    # Не должно бросить наружу.
    asyncio.run(page.route_handler(route))
    assert route.continued == 1, "после сбоя abort должен быть fallback-continue"
