"""Unit-тесты для KN-loader anti-ban рычагов в scrapers/stealth (#1945).

DOM.РФ WAF банит VPS-IP по volume/rate на full sweep (~17k запросов при
concurrency=8). Лечим: (1) per-instance throttle (concurrency + шире jitter)
для KN-sweep, НЕ трогая модульный дефолт остальных скраперов; (2) опциональный
прокси (default off → поведение без изменений).

Тесты проверяют:
  • parse_proxy_url корректно разбирает http://user:pass@host:port;
  • BrowserSession прокидывает concurrency/jitter/proxy в self-поля;
  • дефолтное (None) поведение НЕ меняется (concurrency=8, jitter 600–1500, без прокси).
"""

from __future__ import annotations

import pytest

from app.services.scrapers.stealth import (
    _BROWSER_CONCURRENCY,
    _DEFAULT_JITTER_MAX_MS,
    _DEFAULT_JITTER_MIN_MS,
    BrowserSession,
    parse_proxy_url,
)


class TestParseProxyUrl:
    """parse_proxy_url → Playwright proxy-dict {server, username?, password?}."""

    def test_none_returns_none(self) -> None:
        assert parse_proxy_url(None) is None

    def test_empty_string_returns_none(self) -> None:
        assert parse_proxy_url("") is None

    def test_full_creds_url(self) -> None:
        got = parse_proxy_url("http://user:pass@host.example:8080")
        assert got == {
            "server": "http://host.example:8080",
            "username": "user",
            "password": "pass",
        }

    def test_no_creds(self) -> None:
        got = parse_proxy_url("http://host.example:3128")
        assert got == {"server": "http://host.example:3128"}

    def test_creds_not_leaked_into_server(self) -> None:
        # Playwright требует creds ОТДЕЛЬНЫМИ ключами, не в server-URL.
        # Используем уникальные токены, которых нет в host/scheme.
        got = parse_proxy_url("http://secretuser:secretpass@1.2.3.4:9999")
        assert got is not None
        assert got["server"] == "http://1.2.3.4:9999"
        assert "secretuser" not in got["server"]
        assert "secretpass" not in got["server"]
        assert "@" not in got["server"]

    def test_scheme_preserved(self) -> None:
        got = parse_proxy_url("https://u:p@host:443")
        assert got is not None
        assert got["server"] == "https://host:443"

    def test_default_scheme_when_missing(self) -> None:
        # urlsplit без схемы кладёт всё в path → host не резолвится → ValueError.
        with pytest.raises(ValueError):
            parse_proxy_url("host.example:8080")

    def test_host_required(self) -> None:
        with pytest.raises(ValueError):
            parse_proxy_url("http://:8080")

    def test_error_does_not_leak_credentials(self) -> None:
        # ValueError всплывает в kn_scrape_runs.error / log_progress / Sentry —
        # НЕ должен содержать пароль (#1945 sec-review).
        bad = "http://secretuser:secretpass@:8080"  # есть creds, но нет host
        with pytest.raises(ValueError) as exc:
            parse_proxy_url(bad)
        msg = str(exc.value)
        assert "secretpass" not in msg
        assert "secretuser" not in msg
        assert bad not in msg


class TestBrowserSessionThrottleThreading:
    """concurrency/jitter/proxy прокидываются в инстанс-поля BrowserSession."""

    def test_concurrency_threaded(self) -> None:
        sess = BrowserSession(concurrency=2)
        assert sess._concurrency == 2
        assert sess._sem._value == 2

    def test_jitter_threaded(self) -> None:
        sess = BrowserSession(jitter_min_ms=1200, jitter_max_ms=3000)
        assert sess._jitter_min_ms == 1200
        assert sess._jitter_max_ms == 3000

    def test_proxy_threaded_and_parsed(self) -> None:
        sess = BrowserSession(proxy_url="http://u:p@proxy.local:8000")
        assert sess._proxy == {
            "server": "http://proxy.local:8000",
            "username": "u",
            "password": "p",
        }


class TestBrowserSessionDefaultsUnchanged:
    """None у каждого рычага = старое поведение (другие скраперы НЕ затронуты)."""

    def test_default_concurrency_is_module_default(self) -> None:
        sess = BrowserSession()
        assert sess._concurrency == _BROWSER_CONCURRENCY == 8
        assert sess._sem._value == 8

    def test_default_jitter_is_module_default(self) -> None:
        sess = BrowserSession()
        assert sess._jitter_min_ms == _DEFAULT_JITTER_MIN_MS == 600
        assert sess._jitter_max_ms == _DEFAULT_JITTER_MAX_MS == 1500

    def test_default_proxy_is_none(self) -> None:
        sess = BrowserSession()
        assert sess._proxy is None

    def test_explicit_none_proxy_is_none(self) -> None:
        sess = BrowserSession(proxy_url=None)
        assert sess._proxy is None
