"""Тесты Fix 2: provider-aware _rotate_proxy_ip (#1848).

Тест 1: source='cian' → cian_proxy_rotate_url используется.
Тест 2: source='yandex' → yandex_proxy_rotate_url используется.
Тест 3: source='avito' (default) → avito_proxy_rotate_url (backward compat).
Тест 4: cian без cian url → fallback на avito_proxy_rotate_url.
Тест 5: rotate_url=None → returns False (no HTTP call).
Тест 6: network error → returns False (no crash).
Тест 7: yandex fallback на avito.
"""

from __future__ import annotations

import os
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")

from app.services.scrape_pipeline import _rotate_proxy_ip

# ── Helpers ──────────────────────────────────────────────────────────────────


def _async_session_ctx(resp_json: dict | None = None):
    """Возвращает (async_cm_class, session_mock) для curl_cffi AsyncSession."""
    session = AsyncMock()
    if resp_json is not None:
        mock_resp = MagicMock()
        mock_resp.json.return_value = resp_json
        session.get = AsyncMock(return_value=mock_resp)
    else:
        session.get = AsyncMock(side_effect=RuntimeError("network error"))

    # curl_cffi AsyncSession используется как `async with AsyncSession(...) as rot:`
    # Нам нужен callable класс-замена, вызов которого возвращает async context manager.
    class _MockAsyncSession:
        def __init__(self, timeout=30):
            pass

        async def __aenter__(self):
            return session

        async def __aexit__(self, *_):
            pass

    return _MockAsyncSession, session


# ── Тест 1: cian_proxy_rotate_url ────────────────────────────────────────────


@pytest.mark.asyncio
async def test_rotate_proxy_ip_cian_uses_cian_url() -> None:
    """source='cian' → cian_proxy_rotate_url (не avito_proxy_rotate_url)."""
    mock_session_cls, session = _async_session_ctx(resp_json={"new_ip": "1.2.3.4"})

    with (
        patch("app.services.scrape_pipeline.settings") as s,
        patch("app.services.scrape_pipeline.AsyncSession", mock_session_cls),
        patch("app.services.scrape_pipeline.asyncio.sleep", AsyncMock()),
    ):
        s.cian_proxy_rotate_url = "http://cian-rotate.test/"
        s.avito_proxy_rotate_url = "http://avito-rotate.test/"
        s.yandex_proxy_rotate_url = None
        s.cian_proxy_max_rotations = 4
        s.avito_proxy_max_rotations = 4
        s.yandex_proxy_max_rotations = 4
        s.avito_proxy_rotate_settle_s = 0.0

        result = await _rotate_proxy_ip(reason="ban", rotations_done=0, source="cian")

    assert result is True
    called_url: str = session.get.call_args[0][0]
    assert "cian-rotate.test" in called_url
    assert "avito-rotate.test" not in called_url


# ── Тест 2: yandex_proxy_rotate_url ─────────────────────────────────────────


@pytest.mark.asyncio
async def test_rotate_proxy_ip_yandex_uses_yandex_url() -> None:
    """source='yandex' → yandex_proxy_rotate_url."""
    mock_session_cls, session = _async_session_ctx(resp_json={"new_ip": "5.6.7.8"})

    with (
        patch("app.services.scrape_pipeline.settings") as s,
        patch("app.services.scrape_pipeline.AsyncSession", mock_session_cls),
        patch("app.services.scrape_pipeline.asyncio.sleep", AsyncMock()),
    ):
        s.yandex_proxy_rotate_url = "http://yandex-rotate.test/"
        s.avito_proxy_rotate_url = "http://avito-rotate.test/"
        s.cian_proxy_rotate_url = None
        s.yandex_proxy_max_rotations = 4
        s.avito_proxy_max_rotations = 4
        s.cian_proxy_max_rotations = 4
        s.avito_proxy_rotate_settle_s = 0.0

        result = await _rotate_proxy_ip(reason="timeout", rotations_done=0, source="yandex")

    assert result is True
    called_url: str = session.get.call_args[0][0]
    assert "yandex-rotate.test" in called_url
    assert "avito-rotate.test" not in called_url


# ── Тест 3: avito backward compat ────────────────────────────────────────────


@pytest.mark.asyncio
async def test_rotate_proxy_ip_avito_backward_compat() -> None:
    """source не передан (дефолт 'avito') → avito_proxy_rotate_url."""
    mock_session_cls, session = _async_session_ctx(resp_json={"new_ip": "9.9.9.9"})

    with (
        patch("app.services.scrape_pipeline.settings") as s,
        patch("app.services.scrape_pipeline.AsyncSession", mock_session_cls),
        patch("app.services.scrape_pipeline.asyncio.sleep", AsyncMock()),
    ):
        s.avito_proxy_rotate_url = "http://avito-only.test/"
        s.cian_proxy_rotate_url = None
        s.yandex_proxy_rotate_url = None
        s.avito_proxy_max_rotations = 4
        s.cian_proxy_max_rotations = 4
        s.yandex_proxy_max_rotations = 4
        s.avito_proxy_rotate_settle_s = 0.0

        # Вызов без source= — должен работать как раньше
        result = await _rotate_proxy_ip(reason="block", rotations_done=0)

    assert result is True
    called_url: str = session.get.call_args[0][0]
    assert "avito-only.test" in called_url


# ── Тест 4: cian fallback на avito когда нет cian url ─────────────────────


@pytest.mark.asyncio
async def test_rotate_proxy_ip_cian_fallback_to_avito() -> None:
    """cian_proxy_rotate_url=None → fallback на avito_proxy_rotate_url."""
    mock_session_cls, session = _async_session_ctx(resp_json={})

    with (
        patch("app.services.scrape_pipeline.settings") as s,
        patch("app.services.scrape_pipeline.AsyncSession", mock_session_cls),
        patch("app.services.scrape_pipeline.asyncio.sleep", AsyncMock()),
    ):
        s.cian_proxy_rotate_url = None
        s.avito_proxy_rotate_url = "http://avito-fallback.test/"
        s.yandex_proxy_rotate_url = None
        s.cian_proxy_max_rotations = 4
        s.avito_proxy_max_rotations = 4
        s.yandex_proxy_max_rotations = 4
        s.avito_proxy_rotate_settle_s = 0.0

        result = await _rotate_proxy_ip(reason="ban", rotations_done=0, source="cian")

    assert result is True
    called_url: str = session.get.call_args[0][0]
    assert "avito-fallback.test" in called_url


# ── Тест 5: rotate_url=None → returns False ──────────────────────────────────


@pytest.mark.asyncio
async def test_rotate_proxy_ip_returns_false_when_no_url() -> None:
    """Никакой URL не задан → False, HTTP-запрос не делается."""
    mock_session_cls, session = _async_session_ctx(resp_json={})

    with (
        patch("app.services.scrape_pipeline.settings") as s,
        patch("app.services.scrape_pipeline.AsyncSession", mock_session_cls),
    ):
        s.cian_proxy_rotate_url = None
        s.avito_proxy_rotate_url = None
        s.yandex_proxy_rotate_url = None
        s.cian_proxy_max_rotations = 4
        s.avito_proxy_max_rotations = 4
        s.yandex_proxy_max_rotations = 4

        result = await _rotate_proxy_ip(reason="ban", rotations_done=0, source="cian")

    assert result is False
    # HTTP-запрос не делался
    session.get.assert_not_called()


# ── Тест 6: network error → returns False ────────────────────────────────────


@pytest.mark.asyncio
async def test_rotate_proxy_ip_returns_false_on_network_error() -> None:
    """Ошибка сети → returns False (не крашит caller)."""
    mock_session_cls, _session = _async_session_ctx(resp_json=None)  # raises RuntimeError

    with (
        patch("app.services.scrape_pipeline.settings") as s,
        patch("app.services.scrape_pipeline.AsyncSession", mock_session_cls),
        patch("app.services.scrape_pipeline.asyncio.sleep", AsyncMock()),
    ):
        s.cian_proxy_rotate_url = "http://cian-rotate.test/"
        s.avito_proxy_rotate_url = None
        s.yandex_proxy_rotate_url = None
        s.cian_proxy_max_rotations = 4
        s.avito_proxy_max_rotations = 4
        s.yandex_proxy_max_rotations = 4
        s.avito_proxy_rotate_settle_s = 0.0
        s.proxy_rotate_attempts = 1  # одна попытка, без retry-sleep (#1950)
        s.proxy_rotate_attempt_timeout_s = 8.0

        result = await _rotate_proxy_ip(reason="ban", rotations_done=0, source="cian")

    assert result is False


# ── Тест 7: yandex fallback на avito ─────────────────────────────────────────


@pytest.mark.asyncio
async def test_rotate_proxy_ip_yandex_fallback_to_avito() -> None:
    """yandex_proxy_rotate_url=None → fallback на avito_proxy_rotate_url."""
    mock_session_cls, session = _async_session_ctx(resp_json={"new_ip": "11.22.33.44"})

    with (
        patch("app.services.scrape_pipeline.settings") as s,
        patch("app.services.scrape_pipeline.AsyncSession", mock_session_cls),
        patch("app.services.scrape_pipeline.asyncio.sleep", AsyncMock()),
    ):
        s.yandex_proxy_rotate_url = None
        s.avito_proxy_rotate_url = "http://shared-proxy.test/"
        s.cian_proxy_rotate_url = None
        s.yandex_proxy_max_rotations = 4
        s.avito_proxy_max_rotations = 4
        s.cian_proxy_max_rotations = 4
        s.avito_proxy_rotate_settle_s = 0.0

        result = await _rotate_proxy_ip(reason="503", rotations_done=0, source="yandex")

    assert result is True
    called_url: str = session.get.call_args[0][0]
    assert "shared-proxy.test" in called_url
