"""Интеграционные тесты `/api/v1/admin/*` («Админ-API», блок C).

Покрывает: 403 не-админу на все эндпоинты; CRUD конференций (реюз
`ConferenceService`); PATCH пользователя (роль/блокировка), запрет
самоизменения (409); заблокированный пользователь получает 401 на защищённых
эндпоинтах; ручная рассылка приглашений ставит задачу (мок продюсера);
`PUT /admin/settings` с недоступным уровнем AI → 400; частичный PUT настроек.
"""

import uuid
from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock

import httpx
import pytest
from sqlalchemy.ext.asyncio import AsyncSession

import api.admin as admin_module
from core.config import get_settings
from core.security import create_access_token, hash_password
from models.conference import Conference
from models.team import Team
from models.user import User
from services import ai_levels
from services.conference_ids import generate_number, generate_slug
from services.email import EmailSendError

FUTURE = datetime.now(UTC) + timedelta(days=3)


async def _make_user(session: AsyncSession, *, role: str = "user") -> User:
    user = User(
        email=f"{uuid.uuid4()}@example.com",
        name_user="Admin API Tester",
        password_hash=await hash_password("password123"),
        email_verified=True,
        role=role,
    )
    session.add(user)
    await session.flush()
    return user


def _auth_headers(user: User) -> dict[str, str]:
    token = create_access_token(user.id, user.role)
    return {"Authorization": f"Bearer {token}"}


async def _make_conference(
    session: AsyncSession, *, owner_id: uuid.UUID | None = None, **overrides: object
) -> Conference:
    defaults: dict[str, object] = {
        "number": generate_number(),
        "slug": generate_slug(),
        "owner_id": owner_id,
        "status": "scheduled",
        "scheduled_at": FUTURE,
    }
    defaults.update(overrides)
    conference = Conference(**defaults)
    session.add(conference)
    await session.flush()
    return conference


# --- 403 не-админу -----------------------------------------------------------------


async def test_all_admin_endpoints_forbidden_for_non_admin(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    user = await _make_user(db_session)
    conference = await _make_conference(db_session, owner_id=user.id)
    other_user = await _make_user(db_session)
    await db_session.commit()
    headers = _auth_headers(user)

    requests = [
        ("GET", "/api/v1/admin/conferences", None),
        ("PATCH", f"/api/v1/admin/conferences/{conference.id}", {"title": "x"}),
        ("DELETE", f"/api/v1/admin/conferences/{conference.id}", None),
        ("POST", f"/api/v1/admin/conferences/{conference.id}/invitations", {}),
        ("GET", "/api/v1/admin/users", None),
        ("GET", f"/api/v1/admin/users/{other_user.id}", None),
        ("PATCH", f"/api/v1/admin/users/{other_user.id}", {"role": "admin"}),
        ("GET", "/api/v1/admin/settings", None),
        ("PUT", "/api/v1/admin/settings", {}),
        ("POST", "/api/v1/admin/settings/test-email", {}),
    ]
    for method, path, body in requests:
        response = await client.request(method, path, json=body, headers=headers)
        assert response.status_code == 403, f"{method} {path} -> {response.status_code}"


async def test_admin_endpoints_require_authentication(client: httpx.AsyncClient) -> None:
    response = await client.get("/api/v1/admin/conferences")
    assert response.status_code == 401


# --- Конференции ---------------------------------------------------------------------


async def test_list_conferences_returns_all(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    owner = await _make_user(db_session)
    conference = await _make_conference(db_session, owner_id=owner.id, title="Sync")
    await db_session.commit()

    response = await client.get("/api/v1/admin/conferences", headers=_auth_headers(admin))
    assert response.status_code == 200, response.text
    body = response.json()
    ids = [item["id"] for item in body["items"]]
    assert str(conference.id) in ids
    assert body["total"] >= 1

    item = next(item for item in body["items"] if item["id"] == str(conference.id))
    assert item["owner_name"] == owner.name_user
    assert item["owner_email"] == owner.email


async def test_list_conferences_owner_fields_are_null_without_owner(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    conference = await _make_conference(db_session, owner_id=None, title="Ownerless")
    await db_session.commit()

    response = await client.get("/api/v1/admin/conferences", headers=_auth_headers(admin))
    assert response.status_code == 200, response.text
    item = next(item for item in response.json()["items"] if item["id"] == str(conference.id))
    assert item["owner_name"] is None
    assert item["owner_email"] is None


async def test_list_conferences_filters_by_status_and_query(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    owner = await _make_user(db_session)
    await _make_conference(db_session, owner_id=owner.id, title="Findable Sync", status="scheduled")
    await _make_conference(db_session, owner_id=owner.id, title="Other", status="ended")
    await db_session.commit()

    response = await client.get(
        "/api/v1/admin/conferences",
        params={"status": "scheduled", "q": "Findable"},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 200
    titles = [item["title"] for item in response.json()["items"]]
    assert titles == ["Findable Sync"]


async def test_patch_conference_by_admin_returns_200(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    owner = await _make_user(db_session)
    conference = await _make_conference(db_session, owner_id=owner.id, title="Original")
    await db_session.commit()

    response = await client.patch(
        f"/api/v1/admin/conferences/{conference.id}",
        json={"title": "Renamed by admin"},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["title"] == "Renamed by admin"
    assert body["owner_name"] == owner.name_user
    assert body["owner_email"] == owner.email


async def test_patch_unknown_conference_returns_404(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.patch(
        f"/api/v1/admin/conferences/{uuid.uuid4()}",
        json={"title": "x"},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 404


async def test_delete_conference_by_admin_returns_204(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    owner = await _make_user(db_session)
    conference = await _make_conference(db_session, owner_id=owner.id, status="scheduled")
    await db_session.commit()

    response = await client.delete(
        f"/api/v1/admin/conferences/{conference.id}", headers=_auth_headers(admin)
    )
    assert response.status_code == 204


async def test_delete_active_conference_by_admin_returns_409(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    owner = await _make_user(db_session)
    conference = await _make_conference(
        db_session, owner_id=owner.id, status="active", scheduled_at=None
    )
    await db_session.commit()

    response = await client.delete(
        f"/api/v1/admin/conferences/{conference.id}", headers=_auth_headers(admin)
    )
    assert response.status_code == 409


async def test_send_invitations_enqueues_task(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    admin = await _make_user(db_session, role="admin")
    owner = await _make_user(db_session)
    conference = await _make_conference(db_session, owner_id=owner.id)
    await db_session.commit()

    mock_enqueue = MagicMock()
    monkeypatch.setattr(admin_module, "enqueue_invitations", mock_enqueue)

    response = await client.post(
        f"/api/v1/admin/conferences/{conference.id}/invitations",
        json={"emails": ["custom@example.com"]},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 202, response.text
    mock_enqueue.assert_called_once_with(conference.id, emails=["custom@example.com"])


async def test_send_invitations_unknown_conference_returns_404(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()
    monkeypatch.setattr(admin_module, "enqueue_invitations", MagicMock())

    response = await client.post(
        f"/api/v1/admin/conferences/{uuid.uuid4()}/invitations",
        json={},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 404


# --- Создание пользователя администратором ---------------------------------------------


async def test_create_user_returns_201_and_login_works(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()
    email = f"{uuid.uuid4()}@example.com"

    response = await client.post(
        "/api/v1/admin/users",
        json={"name_user": "Новый пользователь", "email": email, "password": "password123"},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 201, response.text
    body = response.json()
    assert body["email"] == email
    assert body["name_user"] == "Новый пользователь"
    assert body["role"] == "user"
    assert body["email_verified"] is True

    login = await client.post(
        "/api/v1/auth/token", data={"username": email, "password": "password123"}
    )
    assert login.status_code == 200, login.text
    assert "access_token" in login.json()


async def test_create_user_with_team_id(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    team = Team(name=f"Team {uuid.uuid4()}")
    db_session.add(team)
    await db_session.flush()
    await db_session.commit()
    email = f"{uuid.uuid4()}@example.com"

    response = await client.post(
        "/api/v1/admin/users",
        json={
            "name_user": "С командой",
            "email": email,
            "password": "password123",
            "team_id": str(team.id),
        },
        headers=_auth_headers(admin),
    )
    assert response.status_code == 201, response.text
    assert response.json()["team_id"] == str(team.id)


async def test_create_user_unknown_team_returns_404(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.post(
        "/api/v1/admin/users",
        json={
            "name_user": "Без команды",
            "email": f"{uuid.uuid4()}@example.com",
            "password": "password123",
            "team_id": str(uuid.uuid4()),
        },
        headers=_auth_headers(admin),
    )
    assert response.status_code == 404
    assert response.json()["detail"] == "team_not_found"


async def test_create_user_duplicate_email_returns_409(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    existing = await _make_user(db_session)
    await db_session.commit()

    response = await client.post(
        "/api/v1/admin/users",
        json={
            "name_user": "Дубль",
            "email": existing.email,
            "password": "password123",
        },
        headers=_auth_headers(admin),
    )
    assert response.status_code == 409
    assert response.json()["detail"] == "email_already_registered"


async def test_create_user_forbidden_for_non_admin(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    user = await _make_user(db_session)
    await db_session.commit()

    response = await client.post(
        "/api/v1/admin/users",
        json={
            "name_user": "Not allowed",
            "email": f"{uuid.uuid4()}@example.com",
            "password": "password123",
        },
        headers=_auth_headers(user),
    )
    assert response.status_code == 403


# --- Пользователи ----------------------------------------------------------------------


async def test_list_users_returns_all(client: httpx.AsyncClient, db_session: AsyncSession) -> None:
    admin = await _make_user(db_session, role="admin")
    other = await _make_user(db_session)
    await db_session.commit()

    response = await client.get("/api/v1/admin/users", headers=_auth_headers(admin))
    assert response.status_code == 200
    items = response.json()["items"]
    ids = [item["id"] for item in items]
    assert str(admin.id) in ids
    assert str(other.id) in ids
    assert all("email_verified" in item for item in items)


async def test_list_users_filters_by_status(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """`status=active`/`blocked` фильтрует по `is_blocked`; без параметра — все."""
    admin = await _make_user(db_session, role="admin")
    active_user = await _make_user(db_session)
    blocked_user = await _make_user(db_session)
    blocked_user.is_blocked = True
    await db_session.commit()

    active_response = await client.get(
        "/api/v1/admin/users", params={"status": "active"}, headers=_auth_headers(admin)
    )
    assert active_response.status_code == 200, active_response.text
    active_ids = [item["id"] for item in active_response.json()["items"]]
    assert str(active_user.id) in active_ids
    assert str(blocked_user.id) not in active_ids

    blocked_response = await client.get(
        "/api/v1/admin/users", params={"status": "blocked"}, headers=_auth_headers(admin)
    )
    assert blocked_response.status_code == 200, blocked_response.text
    blocked_ids = [item["id"] for item in blocked_response.json()["items"]]
    assert str(blocked_user.id) in blocked_ids
    assert str(active_user.id) not in blocked_ids

    all_response = await client.get("/api/v1/admin/users", headers=_auth_headers(admin))
    all_ids = [item["id"] for item in all_response.json()["items"]]
    assert str(active_user.id) in all_ids
    assert str(blocked_user.id) in all_ids


async def test_list_users_search_matches_team_name(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """`q` находит пользователей по (части) названия их команды; пользователи
    без команды при этом не пропадают из общей (без поиска) выдачи."""
    admin = await _make_user(db_session, role="admin")
    suffix = uuid.uuid4().hex[:8]
    team = Team(name=f"Rocket-{suffix} Squad")
    db_session.add(team)
    await db_session.flush()
    teamed_user = await _make_user(db_session)
    teamed_user.team_id = team.id
    teamless_user = await _make_user(db_session)
    await db_session.commit()

    response = await client.get(
        "/api/v1/admin/users", params={"q": f"Rocket-{suffix}"}, headers=_auth_headers(admin)
    )
    assert response.status_code == 200, response.text
    ids = [item["id"] for item in response.json()["items"]]
    assert str(teamed_user.id) in ids
    assert str(teamless_user.id) not in ids
    assert str(admin.id) not in ids

    all_response = await client.get("/api/v1/admin/users", headers=_auth_headers(admin))
    all_ids = [item["id"] for item in all_response.json()["items"]]
    assert str(teamless_user.id) in all_ids


async def test_patch_user_role_and_block(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    target = await _make_user(db_session)
    await db_session.commit()

    response = await client.patch(
        f"/api/v1/admin/users/{target.id}",
        json={"role": "admin", "is_blocked": True},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["role"] == "admin"
    assert body["is_blocked"] is True
    assert body["email_verified"] is True


async def test_get_user_returns_profile_card(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """Карточка профиля из таблицы пользователей — те же поля, что `/users/me`."""
    admin = await _make_user(db_session, role="admin")
    target = await _make_user(db_session)
    await db_session.commit()

    response = await client.get(f"/api/v1/admin/users/{target.id}", headers=_auth_headers(admin))
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["id"] == str(target.id)
    assert body["name_user"] == target.name_user
    assert body["avatar_url"] is None
    assert body["team_name"] is None


async def test_get_unknown_user_returns_404(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.get(f"/api/v1/admin/users/{uuid.uuid4()}", headers=_auth_headers(admin))
    assert response.status_code == 404


async def test_patch_user_updates_name(client: httpx.AsyncClient, db_session: AsyncSession) -> None:
    admin = await _make_user(db_session, role="admin")
    target = await _make_user(db_session)
    await db_session.commit()

    response = await client.patch(
        f"/api/v1/admin/users/{target.id}",
        json={"name_user": "Renamed By Admin"},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 200, response.text
    assert response.json()["name_user"] == "Renamed By Admin"


async def test_upload_user_avatar_by_admin(
    client: httpx.AsyncClient,
    db_session: AsyncSession,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("MEDIA_ROOT", str(tmp_path))
    get_settings.cache_clear()
    try:
        admin = await _make_user(db_session, role="admin")
        target = await _make_user(db_session)
        await db_session.commit()

        response = await client.post(
            f"/api/v1/admin/users/{target.id}/avatar",
            headers=_auth_headers(admin),
            files={"file": ("avatar.png", b"\x89PNG\r\n\x1a\n" + b"\x00" * 32, "image/png")},
        )
        assert response.status_code == 200, response.text
        assert response.json()["avatar_url"] is not None
    finally:
        get_settings.cache_clear()


async def test_upload_user_avatar_forbidden_for_non_admin(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    user = await _make_user(db_session)
    target = await _make_user(db_session)
    await db_session.commit()

    response = await client.post(
        f"/api/v1/admin/users/{target.id}/avatar",
        headers=_auth_headers(user),
        files={"file": ("avatar.png", b"\x89PNG\r\n\x1a\n" + b"\x00" * 32, "image/png")},
    )
    assert response.status_code == 403


async def test_patch_self_returns_409(client: httpx.AsyncClient, db_session: AsyncSession) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.patch(
        f"/api/v1/admin/users/{admin.id}",
        json={"is_blocked": True},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 409


async def test_blocked_user_gets_401_on_protected_endpoint(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    target = await _make_user(db_session)
    await db_session.commit()
    target_headers = _auth_headers(target)

    block_response = await client.patch(
        f"/api/v1/admin/users/{target.id}",
        json={"is_blocked": True},
        headers=_auth_headers(admin),
    )
    assert block_response.status_code == 200

    response = await client.get("/api/v1/users/me", headers=target_headers)
    assert response.status_code == 401


async def test_patch_unknown_user_returns_404(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.patch(
        f"/api/v1/admin/users/{uuid.uuid4()}",
        json={"is_blocked": True},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 404


# --- Настройки --------------------------------------------------------------------------


async def test_get_settings_includes_ai_levels(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Плумбинг `detect_ai_levels` через API — не тест самого детекта (см.
    `test_ai_levels.py`): железо теста-«пресета 3» смоделировано env-переменными
    (RAM хватает на `min`, но не на `medium`; GPU нет — `max` недоступен), файлы
    моделей считаются скачанными (изолируем тест от реального диска)."""
    monkeypatch.setattr(ai_levels, "_model_downloaded", lambda _path: True)
    monkeypatch.setattr(admin_module, "transcription_queue_served", lambda: False)
    monkeypatch.setenv("HW_RAM_MB", "16384")
    monkeypatch.delenv("HW_GPU_NAME", raising=False)
    get_settings.cache_clear()
    try:
        admin = await _make_user(db_session, role="admin")
        await db_session.commit()

        response = await client.get("/api/v1/admin/settings", headers=_auth_headers(admin))
        assert response.status_code == 200, response.text
        body = response.json()
        levels = {item["level"]: item["available"] for item in body["ai_levels"]}
        assert levels["min"] is True
        assert levels["medium"] is False
        assert levels["max"] is False
    finally:
        get_settings.cache_clear()


async def test_get_settings_transcription_queue_served_reflects_worker_presence(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    """`transcription_queue_served`
    в ответе `GET /admin/settings` для обоих исходов детекта воркера (замокан
    на уровне сервисной функции — сам детект Celery inspect покрыт
    `test_task_producers.py`)."""
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    monkeypatch.setattr(admin_module, "transcription_queue_served", lambda: True)
    served = await client.get("/api/v1/admin/settings", headers=_auth_headers(admin))
    assert served.status_code == 200, served.text
    assert served.json()["transcription_queue_served"] is True

    monkeypatch.setattr(admin_module, "transcription_queue_served", lambda: False)
    not_served = await client.get("/api/v1/admin/settings", headers=_auth_headers(admin))
    assert not_served.status_code == 200, not_served.text
    assert not_served.json()["transcription_queue_served"] is False


async def test_put_settings_partial_update(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(admin_module, "transcription_queue_served", lambda: False)
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.put(
        "/api/v1/admin/settings",
        json={
            "chat_enabled": False,
            "hand_queue_enabled": False,
            "display_timezone": "Asia/Yekaterinburg",
        },
        headers=_auth_headers(admin),
    )
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["chat_enabled"] is False
    assert body["hand_queue_enabled"] is False
    assert body["display_timezone"] == "Asia/Yekaterinburg"
    assert body["ai_level"] == "min"
    assert body["transcription_queue_served"] is False


async def test_put_settings_unavailable_ai_level_returns_400(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.put(
        "/api/v1/admin/settings",
        json={"ai_level": "medium"},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 400


async def test_put_settings_contact_email_enable_and_persist(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(admin_module, "transcription_queue_served", lambda: False)
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.put(
        "/api/v1/admin/settings",
        json={"contact_email_enabled": True, "contact_email": "Contact@VidConf.RU"},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["contact_email_enabled"] is True
    assert body["contact_email"] == "contact@vidconf.ru"

    reloaded = await client.get("/api/v1/admin/settings", headers=_auth_headers(admin))
    assert reloaded.json()["contact_email"] == "contact@vidconf.ru"


async def test_put_settings_contact_email_invalid_returns_400(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.put(
        "/api/v1/admin/settings",
        json={"contact_email_enabled": True, "contact_email": "not an email"},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 400


async def test_get_settings_media_limits_defaults(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Дефолты рычагов нагрузки — без ограничения качества и с текущим
    максимумом сетки (5×5) — существующие инсталляции после обновления не
    получают внезапно ухудшенное качество."""
    monkeypatch.setattr(admin_module, "transcription_queue_served", lambda: False)
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.get("/api/v1/admin/settings", headers=_auth_headers(admin))
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["publish_quality_cap"] == "off"
    assert body["stage_max_tiles"] == 25


async def test_put_settings_media_limits_partial_update(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setattr(admin_module, "transcription_queue_served", lambda: False)
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    response = await client.put(
        "/api/v1/admin/settings",
        json={"publish_quality_cap": "360p", "stage_max_tiles": 9},
        headers=_auth_headers(admin),
    )
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["publish_quality_cap"] == "360p"
    assert body["stage_max_tiles"] == 9

    reloaded = await client.get("/api/v1/admin/settings", headers=_auth_headers(admin))
    assert reloaded.json()["publish_quality_cap"] == "360p"
    assert reloaded.json()["stage_max_tiles"] == 9


async def test_put_settings_media_limits_invalid_values_return_422(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """Значения вне разрешённого набора (`Literal`) — ошибка валидации тела запроса
    ДО сервисного слоя, ещё на уровне FastAPI/pydantic."""
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    bad_cap = await client.put(
        "/api/v1/admin/settings",
        json={"publish_quality_cap": "4k"},
        headers=_auth_headers(admin),
    )
    assert bad_cap.status_code == 422

    bad_tiles = await client.put(
        "/api/v1/admin/settings",
        json={"stage_max_tiles": 100},
        headers=_auth_headers(admin),
    )
    assert bad_tiles.status_code == 422


# --- Тестовое письмо ----------------------------------------------------------------


async def test_send_test_email_defaults_to_admin_email(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Без явного `to` тестовое письмо уходит на email текущего администратора."""
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    sent: dict[str, object] = {}

    class _FakeBackend:
        async def send(self, **kwargs: object) -> None:
            sent.update(kwargs)

    monkeypatch.setattr(admin_module, "create_email_backend", lambda settings: _FakeBackend())

    response = await client.post(
        "/api/v1/admin/settings/test-email", json={}, headers=_auth_headers(admin)
    )

    assert response.status_code == 200, response.text
    body = response.json()
    assert body["success"] is True
    assert sent["to"] == admin.email


async def test_send_test_email_to_explicit_recipient(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()

    sent: dict[str, object] = {}

    class _FakeBackend:
        async def send(self, **kwargs: object) -> None:
            sent.update(kwargs)

    monkeypatch.setattr(admin_module, "create_email_backend", lambda settings: _FakeBackend())

    response = await client.post(
        "/api/v1/admin/settings/test-email",
        json={"to": "other@example.com"},
        headers=_auth_headers(admin),
    )

    assert response.status_code == 200, response.text
    assert sent["to"] == "other@example.com"


async def test_send_test_email_uses_contact_email_as_reply_to(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    admin = await _make_user(db_session, role="admin")
    await db_session.commit()
    await client.put(
        "/api/v1/admin/settings",
        json={"contact_email_enabled": True, "contact_email": "contact@vidconf.example"},
        headers=_auth_headers(admin),
    )

    sent: dict[str, object] = {}

    class _FakeBackend:
        async def send(self, **kwargs: object) -> None:
            sent.update(kwargs)

    monkeypatch.setattr(admin_module, "create_email_backend", lambda settings: _FakeBackend())

    response = await client.post(
        "/api/v1/admin/settings/test-email", json={}, headers=_auth_headers(admin)
    )

    assert response.status_code == 200, response.text
    assert sent["reply_to"] == "contact@vidconf.example"


async def test_send_test_email_reports_transport_failure_without_leaking_secrets(
    client: httpx.AsyncClient, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Сбой транспорта — понятный текст ошибки; хост/порт видны, логин/пароль — нет."""
    monkeypatch.setenv("EMAIL_BACKEND", "smtp")
    monkeypatch.setenv("SMTP_HOST", "smtp.example.com")
    monkeypatch.setenv("SMTP_PORT", "2525")
    monkeypatch.setenv("SMTP_USERNAME", "secret-user")
    monkeypatch.setenv("SMTP_PASSWORD", "super-secret-password")
    get_settings.cache_clear()

    class _FailingBackend:
        async def send(self, **kwargs: object) -> None:
            raise EmailSendError("временный сбой SMTP: сервер недоступен", retryable=True)

    monkeypatch.setattr(admin_module, "create_email_backend", lambda settings: _FailingBackend())
    try:
        admin = await _make_user(db_session, role="admin")
        await db_session.commit()

        response = await client.post(
            "/api/v1/admin/settings/test-email", json={}, headers=_auth_headers(admin)
        )

        assert response.status_code == 200, response.text
        body = response.json()
        assert body["success"] is False
        assert "сбой SMTP" in body["message"]
        assert body["smtp_host"] == "smtp.example.com"
        assert body["smtp_port"] == 2525
        assert "secret-user" not in response.text
        assert "super-secret-password" not in response.text
    finally:
        get_settings.cache_clear()
