"""Интеграционные тесты для `/api/v1/auth`: полный цикл регистрация → вход,
ротация/reuse refresh-токена, закрытые эндпоинты без JWT.
"""

import re
import uuid
from collections.abc import AsyncGenerator, Sequence
from typing import Annotated

import httpx
import pytest_asyncio
from argon2 import PasswordHasher
from fastapi import Depends, FastAPI
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from api.auth import get_auth_service
from core.db import get_session
from core.redis import redis_client
from core.security import needs_rehash
from models.team import Team
from models.user import User
from services.auth import AuthService
from services.email import EmailAttachment
from services.instance_settings import InstanceSettingsService, SettingsUpdateIn

REFRESH_COOKIE = "refresh_token"


@pytest_asyncio.fixture(autouse=True)
async def _reset_registration_gating(db_session: AsyncSession) -> None:
    """Сбросить перед каждым тестом настройки, влияющие на публичную регистрацию.

    `registration_team_choice`/`registration_email_domain_enabled` — общая
    dev-БД, строки могут быть легитимными данными разработчика (включены
    вручную через админку) — тесты, которым это не важно, не должны зависеть
    от текущего состояния общей БД. Тесты, которым важно обратное, переключают
    настройку сами внутри тела теста уже после этого сброса (та же
    транзакция — `db_session` никогда не коммитится в реальную БД, см.
    `conftest.py`).
    """
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(
            registration_team_choice=False,
            registration_email_domain_enabled=False,
            consent_required=False,
        )
    )
    await db_session.commit()


class _CapturingEmailBackend:
    """Тестовый `EmailBackend`: сохраняет отправленные письма в памяти вместо лога."""

    def __init__(self) -> None:
        self.sent: list[tuple[str, str, str]] = []
        self.reply_to: list[str | None] = []

    async def send(
        self,
        *,
        to: str,
        subject: str,
        body: str,
        html_body: str | None = None,
        attachments: Sequence[EmailAttachment] = (),
        reply_to: str | None = None,
    ) -> None:
        self.sent.append((to, subject, body))
        self.reply_to.append(reply_to)


def _extract_verification_token(email_body: str) -> str:
    match = re.search(r"token=([\w\-]+)", email_body)
    assert match is not None, f"токен подтверждения не найден в письме: {email_body!r}"
    return match.group(1)


@pytest_asyncio.fixture
async def email_backend(app: FastAPI) -> AsyncGenerator[_CapturingEmailBackend, None]:
    """Подменить email-бэкенд `AuthService` на перехватывающий (для чтения ссылки verify).

    `app` — свежий экземпляр на каждый тест (фикстура function-scoped), поэтому
    override не нужно откатывать вручную.
    """
    backend = _CapturingEmailBackend()

    async def _get_auth_service_override(
        session: Annotated[AsyncSession, Depends(get_session)],
    ) -> AuthService:
        return AuthService(session=session, redis=redis_client, email_backend=backend)

    app.dependency_overrides[get_auth_service] = _get_auth_service_override
    yield backend


async def _register_and_verify(
    client: httpx.AsyncClient,
    email_backend: _CapturingEmailBackend,
    *,
    email: str = "alice@example.com",
    name_user: str = "Alice",
    password: str = "supersecret1",
) -> None:
    response = await client.post(
        "/api/v1/auth/register",
        json={"email": email, "name_user": name_user, "password": password},
    )
    assert response.status_code == 201, response.text

    assert email_backend.sent, "письмо с подтверждением не было отправлено"
    _, _, body = email_backend.sent[-1]
    token = _extract_verification_token(body)

    verify_response = await client.post("/api/v1/auth/verify-email", json={"token": token})
    assert verify_response.status_code == 204, verify_response.text


async def test_full_cycle_register_verify_login_me(
    client: httpx.AsyncClient, email_backend: _CapturingEmailBackend
) -> None:
    """Регистрация → подтверждение email → вход → GET /users/me с access-токеном."""
    await _register_and_verify(client, email_backend)

    login_response = await client.post(
        "/api/v1/auth/token",
        data={"username": "alice@example.com", "password": "supersecret1"},
    )
    assert login_response.status_code == 200, login_response.text
    body = login_response.json()
    assert body["token_type"] == "bearer"
    access_token = body["access_token"]
    assert REFRESH_COOKIE in login_response.cookies

    me_response = await client.get(
        "/api/v1/users/me", headers={"Authorization": f"Bearer {access_token}"}
    )
    assert me_response.status_code == 200
    me_body = me_response.json()
    assert me_body["email"] == "alice@example.com"
    assert me_body["name_user"] == "Alice"
    assert me_body["role"] == "user"


async def test_register_duplicate_email_returns_409(
    client: httpx.AsyncClient, email_backend: _CapturingEmailBackend
) -> None:
    await _register_and_verify(client, email_backend, email="dup@example.com")

    response = await client.post(
        "/api/v1/auth/register",
        json={"email": "dup@example.com", "name_user": "Dup", "password": "supersecret1"},
    )
    assert response.status_code == 409


async def test_login_without_verification_returns_403(
    client: httpx.AsyncClient, email_backend: _CapturingEmailBackend
) -> None:
    register_response = await client.post(
        "/api/v1/auth/register",
        json={"email": "bob@example.com", "name_user": "Bob", "password": "supersecret1"},
    )
    assert register_response.status_code == 201

    login_response = await client.post(
        "/api/v1/auth/token",
        data={"username": "bob@example.com", "password": "supersecret1"},
    )
    assert login_response.status_code == 403


async def test_verify_email_with_invalid_token_returns_400(
    client: httpx.AsyncClient, email_backend: _CapturingEmailBackend
) -> None:
    response = await client.post("/api/v1/auth/verify-email", json={"token": "does-not-exist"})
    assert response.status_code == 400


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


async def test_refresh_rotates_token_and_rejects_reuse(
    client: httpx.AsyncClient, email_backend: _CapturingEmailBackend
) -> None:
    await _register_and_verify(client, email_backend, email="carol@example.com")
    login_response = await client.post(
        "/api/v1/auth/token",
        data={"username": "carol@example.com", "password": "supersecret1"},
    )
    assert login_response.status_code == 200
    old_refresh_token = client.cookies.get(REFRESH_COOKIE)
    assert old_refresh_token is not None

    refresh_response = await client.post("/api/v1/auth/refresh")
    assert refresh_response.status_code == 200
    new_refresh_token = client.cookies.get(REFRESH_COOKIE)
    assert new_refresh_token is not None
    assert new_refresh_token != old_refresh_token

    # Повторное предъявление уже потраченного (старого) refresh-токена — reuse.
    client.cookies.set(REFRESH_COOKIE, old_refresh_token)
    reuse_response = await client.post("/api/v1/auth/refresh")
    assert reuse_response.status_code == 401


async def test_refresh_without_cookie_returns_401(client: httpx.AsyncClient) -> None:
    response = await client.post("/api/v1/auth/refresh")
    assert response.status_code == 401


async def test_logout_revokes_refresh_token(
    client: httpx.AsyncClient, email_backend: _CapturingEmailBackend
) -> None:
    await _register_and_verify(client, email_backend, email="dave@example.com")
    login_response = await client.post(
        "/api/v1/auth/token",
        data={"username": "dave@example.com", "password": "supersecret1"},
    )
    assert login_response.status_code == 200

    logout_response = await client.post("/api/v1/auth/logout")
    assert logout_response.status_code == 204

    refresh_response = await client.post("/api/v1/auth/refresh")
    assert refresh_response.status_code == 401


# --- Выбор команды при регистрации ---------------------------------------------


async def test_registration_options_disabled_by_default(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """Настройка `registration_team_choice` выключена (см. `_reset_registration_gating`)
    — команды не отдаются."""
    db_session.add(Team(name=f"Zeta Team {uuid.uuid4()}"))
    await db_session.commit()

    response = await client.get("/api/v1/auth/registration-options")
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["team_choice_enabled"] is False
    assert body["teams"] == []
    assert body["email_domains"] == []


async def test_registration_options_enabled_returns_teams_sorted_by_name(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """Список команд отсортирован по названию; проверяем взаимный порядок своих команд
    (не полное равенство списка — в общей dev-БД могут быть и другие команды)."""
    suffix = uuid.uuid4().hex[:8]
    name_alpha, name_zeta = f"Alpha-{suffix}", f"Zeta-{suffix}"
    db_session.add_all([Team(name=name_zeta), Team(name=name_alpha)])
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(registration_team_choice=True)
    )
    await db_session.commit()

    response = await client.get("/api/v1/auth/registration-options")
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["team_choice_enabled"] is True
    names = [team["name"] for team in body["teams"]]
    assert names == sorted(names)
    assert name_alpha in names and name_zeta in names
    assert names.index(name_alpha) < names.index(name_zeta)


async def test_register_without_team_id_still_works(
    client: httpx.AsyncClient, email_backend: _CapturingEmailBackend
) -> None:
    response = await client.post(
        "/api/v1/auth/register",
        json={"email": "no-team@example.com", "name_user": "No Team", "password": "supersecret1"},
    )
    assert response.status_code == 201, response.text


async def test_register_with_team_id_when_enabled_assigns_team(
    client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
    team = Team(name=f"Engineering {uuid.uuid4()}")
    db_session.add(team)
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(registration_team_choice=True)
    )
    await db_session.commit()

    response = await client.post(
        "/api/v1/auth/register",
        json={
            "email": "with-team@example.com",
            "name_user": "With Team",
            "password": "supersecret1",
            "team_id": str(team.id),
        },
    )
    assert response.status_code == 201, response.text

    result = await db_session.execute(select(User).where(User.email == "with-team@example.com"))
    created = result.scalar_one()
    assert created.team_id == team.id


async def test_register_with_team_id_when_disabled_returns_400(
    client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
    # `registration_team_choice` выключена `_reset_registration_gating`.
    team = Team(name=f"Disabled Choice Team {uuid.uuid4()}")
    db_session.add(team)
    await db_session.commit()

    response = await client.post(
        "/api/v1/auth/register",
        json={
            "email": "team-disabled@example.com",
            "name_user": "Team Disabled",
            "password": "supersecret1",
            "team_id": str(team.id),
        },
    )
    assert response.status_code == 400


async def test_register_with_unknown_team_id_returns_400(
    client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(registration_team_choice=True)
    )
    await db_session.commit()

    response = await client.post(
        "/api/v1/auth/register",
        json={
            "email": "unknown-team@example.com",
            "name_user": "Unknown Team",
            "password": "supersecret1",
            "team_id": "00000000-0000-0000-0000-000000000000",
        },
    )
    assert response.status_code == 400


# --- Верификация регистрирующихся по домену email -------------------------------------


async def test_registration_options_returns_email_domains_when_enabled(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """При включённой верификации домена `registration-options` отдаёт все эталонные домены."""
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(
            registration_email_domain_enabled=True,
            registration_email_domains=["example.com", "corp.example"],
        )
    )
    await db_session.commit()

    response = await client.get("/api/v1/auth/registration-options")
    assert response.status_code == 200, response.text
    assert response.json()["email_domains"] == ["example.com", "corp.example"]


async def test_registration_options_email_domains_empty_when_disabled(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """Выключенная верификация домена — `email_domains` всегда пуст, даже если домены сохранены."""
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(
            registration_email_domain_enabled=True, registration_email_domains=["example.com"]
        )
    )
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(registration_email_domain_enabled=False)
    )
    await db_session.commit()

    response = await client.get("/api/v1/auth/registration-options")
    assert response.status_code == 200, response.text
    assert response.json()["email_domains"] == []


async def test_register_with_foreign_domain_when_verification_enabled_returns_400(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(
            registration_email_domain_enabled=True,
            registration_email_domains=["example.com", "corp.example"],
        )
    )
    await db_session.commit()

    response = await client.post(
        "/api/v1/auth/register",
        json={
            "email": "alice@other.com",
            "name_user": "Alice",
            "password": "supersecret1",
        },
    )
    assert response.status_code == 400
    assert response.json()["detail"] == "invalid_email_domain"


async def test_register_with_matching_domain_case_insensitive_succeeds(
    client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(
            registration_email_domain_enabled=True, registration_email_domains=["example.com"]
        )
    )
    await db_session.commit()

    response = await client.post(
        "/api/v1/auth/register",
        json={
            "email": "bob@EXAMPLE.COM",
            "name_user": "Bob",
            "password": "supersecret1",
        },
    )
    assert response.status_code == 201, response.text


async def test_register_with_second_domain_in_list_succeeds(
    client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
    """Email подходит, если совпадает с ЛЮБЫМ доменом из списка — не только с первым."""
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(
            registration_email_domain_enabled=True,
            registration_email_domains=["example.com", "corp.example"],
        )
    )
    await db_session.commit()

    response = await client.post(
        "/api/v1/auth/register",
        json={
            "email": "dave@corp.example",
            "name_user": "Dave",
            "password": "supersecret1",
        },
    )
    assert response.status_code == 201, response.text


async def test_register_any_domain_allowed_when_verification_disabled(
    client: httpx.AsyncClient, email_backend: _CapturingEmailBackend
) -> None:
    """Выключенная верификация (`_reset_registration_gating`) — любой домен email
    проходит регистрацию."""
    response = await client.post(
        "/api/v1/auth/register",
        json={
            "email": "carol@anything.example",
            "name_user": "Carol",
            "password": "supersecret1",
        },
    )
    assert response.status_code == 201, response.text


async def test_register_sets_reply_to_when_contact_email_enabled(
    client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
    """Контактный адрес инстанса включён — письмо подтверждения регистрации несёт `Reply-To`."""
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(contact_email_enabled=True, contact_email="contact@vidconf.example")
    )
    await db_session.commit()

    response = await client.post(
        "/api/v1/auth/register",
        json={"email": "dave@example.com", "name_user": "Dave", "password": "supersecret1"},
    )

    assert response.status_code == 201, response.text
    assert email_backend.reply_to[-1] == "contact@vidconf.example"


async def test_register_no_reply_to_when_contact_email_disabled(
    client: httpx.AsyncClient, email_backend: _CapturingEmailBackend
) -> None:
    """Контактный адрес выключен (дефолт) — `Reply-To` не проставляется."""
    response = await client.post(
        "/api/v1/auth/register",
        json={"email": "erin@example.com", "name_user": "Erin", "password": "supersecret1"},
    )

    assert response.status_code == 201, response.text
    assert email_backend.reply_to[-1] is None


async def test_registration_options_returns_consent_fields(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    """`consent_text`/`consent_version` отдаются ВСЕГДА (нужны странице регламента),
    `consent_required` — по факту настройки инстанса."""
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(consent_policy_text="Текст регламента для теста")
    )
    await db_session.commit()

    response = await client.get("/api/v1/auth/registration-options")
    assert response.status_code == 200, response.text
    body = response.json()
    assert body["consent_required"] is False
    assert body["consent_text"] == "Текст регламента для теста"
    assert isinstance(body["consent_version"], int)


async def test_register_without_consent_when_required_returns_400(
    client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
    """Сервер отказывает в регистрации без галочки, даже если фронт её не прислал —
    второй эшелон проверки (тот же принцип, что `hand_queue_disabled` в 0.0.28)."""
    await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(consent_required=True, consent_policy_text="Текст регламента")
    )
    await db_session.commit()

    response = await client.post(
        "/api/v1/auth/register",
        json={
            "email": "no-consent@example.com",
            "name_user": "No Consent",
            "password": "supersecret1",
        },
    )
    assert response.status_code == 400
    assert response.json()["detail"] == "consent_required"

    result = await db_session.execute(select(User).where(User.email == "no-consent@example.com"))
    assert result.scalar_one_or_none() is None


async def test_register_with_consent_when_required_writes_version_and_date(
    client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
    """Принятое согласие пишется в БД вместе с редакцией регламента и датой."""
    cfg = await InstanceSettingsService(db_session).update(
        SettingsUpdateIn(consent_required=True, consent_policy_text="Текст регламента для приёмки")
    )
    await db_session.commit()

    response = await client.post(
        "/api/v1/auth/register",
        json={
            "email": "with-consent@example.com",
            "name_user": "With Consent",
            "password": "supersecret1",
            "consent_accepted": True,
        },
    )
    assert response.status_code == 201, response.text

    result = await db_session.execute(select(User).where(User.email == "with-consent@example.com"))
    created = result.scalar_one()
    assert created.consent_version == cfg.consent_policy_version
    assert created.consent_given_at is not None


async def test_register_without_consent_when_module_disabled_succeeds_and_leaves_it_null(
    client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
    """Модуль выключен (дефолт `_reset_registration_gating`) — регистрация не требует
    галочки, `consent_version`/`consent_given_at` остаются `NULL`."""
    response = await client.post(
        "/api/v1/auth/register",
        json={
            "email": "consent-disabled@example.com",
            "name_user": "Consent Disabled",
            "password": "supersecret1",
        },
    )
    assert response.status_code == 201, response.text

    result = await db_session.execute(
        select(User).where(User.email == "consent-disabled@example.com")
    )
    created = result.scalar_one()
    assert created.consent_version is None
    assert created.consent_given_at is None


async def test_login_rehashes_legacy_password(
    client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
    """Вход с паролем, захэшированным старыми параметрами, перевыпускает хэш.

    Параметры argon2 зашиты в саму строку хэша, поэтому смена настроек
    (0.0.17: дефолты библиотеки → рекомендации OWASP) сама по себе не ускоряет
    проверку уже существующих паролей. Миграция идёт лениво — при первом
    успешном входе, когда открытый пароль есть на руках.
    """
    email = "legacy-hash@example.com"
    password = "supersecret1"
    await _register_and_verify(client, email_backend, email=email, password=password)

    # Подменяем хэш на выданный прежними параметрами (t=3, m=64 МБ, p=4).
    legacy_hash = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4).hash(password)
    user = await db_session.scalar(select(User).where(User.email == email))
    assert user is not None
    user.password_hash = legacy_hash
    await db_session.commit()

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

    await db_session.refresh(user)
    assert user.password_hash != legacy_hash, "старый хэш не был перевыпущен"
    assert "m=19456" in user.password_hash
    assert needs_rehash(user.password_hash) is False
