"""Интеграционные тесты `/api/v1/users`: список пользователей, профиль, аватар, картинки фона."""

import uuid
from collections.abc import Generator
from pathlib import Path

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

from core.config import get_settings
from core.security import create_access_token, hash_password
from models.team import Team
from models.user import User
from services.backgrounds import MAX_BACKGROUNDS_PER_USER

# Минимальные валидные по магическим байтам содержимые (без полноценного
# декодирования — `services/avatars.py` проверяет только сигнатуру/размер).
_JPEG_BYTES = b"\xff\xd8\xff\xe0" + b"\x00" * 32
_PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 32
_WEBP_BYTES = b"RIFF\x00\x00\x00\x00WEBP" + b"\x00" * 32


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


async def _make_team(session: AsyncSession) -> Team:
    team = Team(name=f"Team {uuid.uuid4()}")
    session.add(team)
    await session.flush()
    return team


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


@pytest.fixture
def media_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Generator[Path, None, None]:
    """Подменить `MEDIA_ROOT` на изолированный `tmp_path` на время теста.

    `get_settings()` кэширован (`lru_cache`) — без явного сброса кэша тест
    продолжил бы видеть значение, закэшированное первым же вызовом в сессии
    pytest. Перечисляется в сигнатуре теста ПЕРВЫМ параметром, чтобы pytest
    инициализировал его до `client`/`app` (см. `main.py::create_app`,
    монтирующий `StaticFiles` по значению `media_root` на момент вызова).
    """
    monkeypatch.setenv("MEDIA_ROOT", str(tmp_path))
    get_settings.cache_clear()
    yield tmp_path
    get_settings.cache_clear()


async def test_list_users_returns_id_and_display_name(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    requester = await _make_user(db_session)
    other = await _make_user(db_session)
    await db_session.commit()

    response = await client.get("/api/v1/users", headers=_auth_headers(requester))
    assert response.status_code == 200
    body = response.json()
    ids = {item["id"] for item in body}
    assert str(requester.id) in ids
    assert str(other.id) in ids
    item = next(i for i in body if i["id"] == str(other.id))
    assert item["display_name"] == other.name_user


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


async def test_get_me_with_reserved_tld_email_does_not_500(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """Регрессия: `UserOut.email` — `str`, а не `EmailStr` (иначе 500 `ResponseValidationError`).

    `.local`/`.test` и т.п. — синтаксически валидные, но зарезервированные
    special-use домены (RFC 6761); email-validator (`EmailStr`) их отклоняет,
    хотя в БД такой email мог оказаться и до этого правила (например,
    сид-админ `admin@vidconf.local` в старых окружениях).
    """
    # Уникальный email (не сталкивается с реально засеянным admin@vidconf.local
    # в дев-БД — тесты идут в общей БД под savepoint, а email уникален).
    legacy_email = f"legacy-{uuid.uuid4()}@vidconf.local"
    user = User(
        email=legacy_email,
        name_user="Legacy Admin",
        password_hash=await hash_password("password123"),
        email_verified=True,
        role="admin",
    )
    db_session.add(user)
    await db_session.commit()

    response = await client.get("/api/v1/users/me", headers=_auth_headers(user))
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["email"] == legacy_email
    assert body["role"] == "admin"


# --- Профиль ------------------------------------------------------


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

    response = await client.get("/api/v1/users/me", headers=_auth_headers(user))
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["avatar_url"] is None
    assert body["team_id"] is None
    assert body["team_name"] is None


async def test_patch_me_updates_name_and_team_but_ignores_email(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    user = await _make_user(db_session)
    team = await _make_team(db_session)
    await db_session.commit()
    original_email = user.email

    response = await client.patch(
        "/api/v1/users/me",
        json={"name_user": "Renamed Self", "team_id": str(team.id), "email": "new@example.com"},
        headers=_auth_headers(user),
    )
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["name_user"] == "Renamed Self"
    assert body["team_id"] == str(team.id)
    assert body["team_name"] == team.name
    assert body["email"] == original_email  # email — read-only, тело проигнорировано


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

    response = await client.patch(
        "/api/v1/users/me",
        json={"team_id": str(uuid.uuid4())},
        headers=_auth_headers(user),
    )
    assert response.status_code == 404
    assert response.json()["detail"] == "team_not_found"


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

    await client.patch(
        "/api/v1/users/me", json={"team_id": str(team.id)}, headers=_auth_headers(user)
    )
    response = await client.patch(
        "/api/v1/users/me", json={"team_id": None}, headers=_auth_headers(user)
    )
    assert response.status_code == 200, response.text
    assert response.json()["team_id"] is None


# --- Аватар -------------------------------------------------------


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

    response = await client.post(
        "/api/v1/users/me/avatar",
        headers=_auth_headers(user),
        files={"file": ("avatar.jpg", _JPEG_BYTES, "image/jpeg")},
    )
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["avatar_url"] is not None
    assert body["avatar_url"].startswith(f"/media/avatars/{user.id}.jpg?v=")
    assert (media_root / "avatars" / f"{user.id}.jpg").read_bytes() == _JPEG_BYTES


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

    response = await client.post(
        "/api/v1/users/me/avatar",
        headers=_auth_headers(user),
        files={"file": ("avatar.png", _PNG_BYTES, "image/png")},
    )
    assert response.status_code == 200, response.text
    assert (media_root / "avatars" / f"{user.id}.png").exists()


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

    await client.post(
        "/api/v1/users/me/avatar",
        headers=_auth_headers(user),
        files={"file": ("avatar.jpg", _JPEG_BYTES, "image/jpeg")},
    )
    response = await client.post(
        "/api/v1/users/me/avatar",
        headers=_auth_headers(user),
        files={"file": ("avatar.webp", _WEBP_BYTES, "image/webp")},
    )
    assert response.status_code == 200, response.text
    assert not (media_root / "avatars" / f"{user.id}.jpg").exists()
    assert (media_root / "avatars" / f"{user.id}.webp").exists()


async def test_upload_avatar_spoofed_content_type_returns_415(
    media_root: Path, client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """`Content-Type: image/png`, но байты — JPEG (подделка заголовка клиентом)."""
    user = await _make_user(db_session)
    await db_session.commit()

    response = await client.post(
        "/api/v1/users/me/avatar",
        headers=_auth_headers(user),
        files={"file": ("avatar.png", _JPEG_BYTES, "image/png")},
    )
    assert response.status_code == 415
    assert response.json()["detail"] == "avatar_invalid_type"


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

    response = await client.post(
        "/api/v1/users/me/avatar",
        headers=_auth_headers(user),
        files={"file": ("avatar.txt", b"not-an-image", "text/plain")},
    )
    assert response.status_code == 415


async def test_upload_avatar_too_large_returns_413(
    media_root: Path, client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    user = await _make_user(db_session)
    await db_session.commit()
    oversized = _JPEG_BYTES + b"\x00" * (2 * 1024 * 1024)

    response = await client.post(
        "/api/v1/users/me/avatar",
        headers=_auth_headers(user),
        files={"file": ("avatar.jpg", oversized, "image/jpeg")},
    )
    assert response.status_code == 413
    assert response.json()["detail"] == "avatar_too_large"


async def test_delete_avatar_removes_file_and_clears_url(
    media_root: Path, client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    user = await _make_user(db_session)
    await db_session.commit()
    await client.post(
        "/api/v1/users/me/avatar",
        headers=_auth_headers(user),
        files={"file": ("avatar.jpg", _JPEG_BYTES, "image/jpeg")},
    )

    response = await client.delete("/api/v1/users/me/avatar", headers=_auth_headers(user))
    assert response.status_code == 204
    assert not (media_root / "avatars" / f"{user.id}.jpg").exists()

    me = await client.get("/api/v1/users/me", headers=_auth_headers(user))
    assert me.json()["avatar_url"] is None


# --- Смена пароля ---------------------------------------------------------------------


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

    response = await client.post(
        "/api/v1/users/me/password",
        json={"current_password": "password123", "new_password": "new-password456"},
        headers=_auth_headers(user),
    )
    assert response.status_code == 204, response.text

    new_login = await client.post(
        "/api/v1/auth/token", data={"username": email, "password": "new-password456"}
    )
    assert new_login.status_code == 200, new_login.text

    old_login = await client.post(
        "/api/v1/auth/token", data={"username": email, "password": "password123"}
    )
    assert old_login.status_code == 401


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

    response = await client.post(
        "/api/v1/users/me/password",
        json={"current_password": "wrong-password", "new_password": "new-password456"},
        headers=_auth_headers(user),
    )
    assert response.status_code == 400
    assert response.json()["detail"] == "invalid_current_password"


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

    response = await client.post(
        "/api/v1/users/me/password",
        json={"current_password": "password123", "new_password": "short"},
        headers=_auth_headers(user),
    )
    assert response.status_code == 422


async def test_change_password_requires_auth(client: httpx.AsyncClient) -> None:
    response = await client.post(
        "/api/v1/users/me/password",
        json={"current_password": "a", "new_password": "new-password456"},
    )
    assert response.status_code == 401


# --- Пикер участников: поиск `q` + `avatar_url` -------------------


async def test_list_users_search_by_q_filters_by_name_or_email(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    requester = await _make_user(db_session)
    unique_marker = str(uuid.uuid4())
    match = User(
        email=f"{unique_marker}@example.com",
        name_user=f"Findable {unique_marker}",
        password_hash=await hash_password("password123"),
        email_verified=True,
    )
    db_session.add(match)
    await db_session.commit()

    response = await client.get(
        "/api/v1/users", params={"q": unique_marker}, headers=_auth_headers(requester)
    )
    assert response.status_code == 200, response.text
    ids = {item["id"] for item in response.json()}
    assert str(match.id) in ids
    assert str(requester.id) not in ids


# --- Свои картинки фона (`/users/me/backgrounds`) -----------------


async def _upload_background(
    client: httpx.AsyncClient, user: User, *, name: str = "bg.webp"
) -> httpx.Response:
    return await client.post(
        "/api/v1/users/me/backgrounds",
        headers=_auth_headers(user),
        files={"file": (name, _WEBP_BYTES, "image/webp")},
    )


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

    response = await client.get("/api/v1/users/me/backgrounds", headers=_auth_headers(user))
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["items"] == []
    assert body["limit"] == MAX_BACKGROUNDS_PER_USER


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

    response = await _upload_background(client, user)
    assert response.status_code == 201, response.text
    items = response.json()["items"]
    assert len(items) == 1
    url = items[0]["url"]
    assert url == f"/media/backgrounds/{user.id}/{items[0]['id']}.webp"
    # Файл лежит на диске, в БД только путь — см. `services/backgrounds.py`.
    assert (media_root / url.removeprefix("/media/")).read_bytes() == _WEBP_BYTES


async def test_upload_background_spoofed_content_type_returns_415(
    media_root: Path, client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """`Content-Type: image/webp`, но байты — JPEG: клиенту не верим и здесь."""
    user = await _make_user(db_session)
    await db_session.commit()

    response = await client.post(
        "/api/v1/users/me/backgrounds",
        headers=_auth_headers(user),
        files={"file": ("bg.webp", _JPEG_BYTES, "image/webp")},
    )
    assert response.status_code == 415
    assert response.json()["detail"] == "background_invalid_type"


async def test_upload_background_too_large_returns_413(
    media_root: Path, client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    user = await _make_user(db_session)
    await db_session.commit()
    oversized = _WEBP_BYTES + b"\x00" * (2 * 1024 * 1024)

    response = await client.post(
        "/api/v1/users/me/backgrounds",
        headers=_auth_headers(user),
        files={"file": ("bg.webp", oversized, "image/webp")},
    )
    assert response.status_code == 413
    assert response.json()["detail"] == "background_too_large"


async def test_upload_background_over_limit_returns_409_and_keeps_ten(
    media_root: Path, client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """Лимит держится на СЕРВЕРЕ: интерфейс можно обойти прямым запросом."""
    user = await _make_user(db_session)
    await db_session.commit()
    for _ in range(MAX_BACKGROUNDS_PER_USER):
        assert (await _upload_background(client, user)).status_code == 201

    response = await _upload_background(client, user)
    assert response.status_code == 409
    assert response.json()["detail"] == "background_limit_reached"

    listing = await client.get("/api/v1/users/me/backgrounds", headers=_auth_headers(user))
    assert len(listing.json()["items"]) == MAX_BACKGROUNDS_PER_USER


async def test_delete_background_removes_file_and_row(
    media_root: Path, client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    user = await _make_user(db_session)
    await db_session.commit()
    created = (await _upload_background(client, user)).json()["items"][0]
    file_path = media_root / created["url"].removeprefix("/media/")
    assert file_path.exists()

    response = await client.delete(
        f"/api/v1/users/me/backgrounds/{created['id']}", headers=_auth_headers(user)
    )
    assert response.status_code == 204
    assert not file_path.exists()

    listing = await client.get("/api/v1/users/me/backgrounds", headers=_auth_headers(user))
    assert listing.json()["items"] == []


async def test_delete_other_users_background_returns_404(
    media_root: Path, client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """Знание id чужой картинки не даёт её удалить — владелец в условии запроса."""
    owner = await _make_user(db_session)
    stranger = await _make_user(db_session)
    await db_session.commit()
    created = (await _upload_background(client, owner)).json()["items"][0]

    response = await client.delete(
        f"/api/v1/users/me/backgrounds/{created['id']}", headers=_auth_headers(stranger)
    )
    assert response.status_code == 404
    assert (media_root / created["url"].removeprefix("/media/")).exists()


async def test_backgrounds_list_does_not_leak_other_users_images(
    media_root: Path, client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    owner = await _make_user(db_session)
    stranger = await _make_user(db_session)
    await db_session.commit()
    await _upload_background(client, owner)

    response = await client.get("/api/v1/users/me/backgrounds", headers=_auth_headers(stranger))
    assert response.json()["items"] == []


async def test_backgrounds_require_authentication(
    media_root: Path, client: httpx.AsyncClient
) -> None:
    assert (await client.get("/api/v1/users/me/backgrounds")).status_code == 401
    assert (
        await client.delete(f"/api/v1/users/me/backgrounds/{uuid.uuid4()}")
    ).status_code == 401
