"""Проверка пароля не должна блокировать event loop (регресс после теста 31.07.2026).

Синхронный `verify_password` останавливал весь процесс backend на 95–155 мс.
При массовом входе (около 70 человек разом) это давало p95 логина 7.28 секунды,
33 соединения к БД в состоянии `idle in transaction` при одном активном запросе
и отказы на посторонних ручках — включая вход в конференцию, где пароль вообще
не проверялся. Разбор — `.forcc/LOGIN-BOTTLENECK.md`.

Тесты ниже проверяют не скорость (она зависит от железа), а **свойства**:
event loop остаётся живым, проверки идут параллельно, старые хэши мигрируют.
"""

import asyncio
import time

from argon2 import PasswordHasher

from core.security import hash_password, needs_rehash, verify_password

PASSWORD = "correct-horse-battery-staple"


async def test_verify_password_does_not_block_event_loop() -> None:
    """Пока считается argon2, event loop продолжает обслуживать другие задачи.

    Это главное свойство правки. Фоновая корутина тикает каждую миллисекунду;
    если проверка пароля выполняется синхронно в loop, тиков за её время будет
    ноль или единицы — именно так и вело себя приложение до исправления.
    """
    password_hash = await hash_password(PASSWORD)

    ticks = 0
    stop = False

    async def ticker() -> None:
        nonlocal ticks
        while not stop:
            ticks += 1
            await asyncio.sleep(0.001)

    ticker_task = asyncio.create_task(ticker())
    await asyncio.sleep(0.005)  # даём тикеру стартовать

    ticks_before = ticks
    assert await verify_password(PASSWORD, password_hash) is True
    ticks_during = ticks - ticks_before

    stop = True
    await ticker_task

    # Даже на быстром железе argon2 занимает десятки миллисекунд — за это время
    # loop обязан прокрутить заметное число тиков. Порог намеренно щадящий:
    # при блокировке тиков будет 0–1, а не «мало».
    assert ticks_during >= 5, (
        f"event loop простоял во время проверки пароля: {ticks_during} тиков — "
        "похоже, argon2 снова считается синхронно"
    )


async def test_parallel_verifications_are_concurrent() -> None:
    """Параллельные проверки идут одновременно, а не выстраиваются в очередь.

    argon2-cffi — C-расширение и освобождает GIL, поэтому пул потоков даёт
    настоящий параллелизм. Проверяем, что 8 проверок занимают заметно меньше,
    чем 8 последовательных: иначе массовый вход снова упрётся в сериализацию.
    """
    password_hash = await hash_password(PASSWORD)

    start = time.perf_counter()
    await verify_password(PASSWORD, password_hash)
    single = time.perf_counter() - start

    start = time.perf_counter()
    results = await asyncio.gather(*(verify_password(PASSWORD, password_hash) for _ in range(8)))
    parallel = time.perf_counter() - start

    assert all(results)
    # На 4-ядерном сервере 8 проверок идеально легли бы в 2×single; берём 5×
    # с большим запасом на шум CI и разное железо — важно лишь то, что это
    # НЕ 8× (последовательное выполнение).
    assert parallel < single * 5, (
        f"8 параллельных проверок заняли {parallel:.3f} с при {single:.3f} с на одну — "
        "похоже, они выполняются последовательно"
    )


async def test_wrong_password_is_rejected() -> None:
    """Асинхронная обёртка не сломала саму проверку."""
    password_hash = await hash_password(PASSWORD)
    assert await verify_password(PASSWORD, password_hash) is True
    assert await verify_password("wrong-password", password_hash) is False


async def test_hasher_uses_owasp_parameters() -> None:
    """Параметры argon2id — по рекомендации OWASP, а не дефолт библиотеки.

    Дефолт argon2-cffi (t=3, m=64 МБ, p=4) стоил 95 мс на проверку, причём
    `parallelism=4` занимал все четыре ядра сервера — те же, на которых
    работает LiveKit.
    """
    password_hash = await hash_password(PASSWORD)
    # Параметры зашиты в саму строку хэша: $argon2id$v=19$m=19456,t=2,p=1$...
    assert "m=19456" in password_hash
    assert "t=2" in password_hash
    assert "p=1" in password_hash


async def test_legacy_hash_is_verified_and_marked_for_rehash() -> None:
    """Хэш со старыми параметрами проверяется, но помечается на перевыпуск.

    Гарантия обратной совместимости: пароли, выданные до смены параметров,
    продолжают работать. `AuthService.login` перевыпускает такой хэш при
    первом же успешном входе — другого момента, когда открытый пароль есть
    на руках, не будет.
    """
    legacy_hasher = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
    legacy_hash = legacy_hasher.hash(PASSWORD)

    assert await verify_password(PASSWORD, legacy_hash) is True
    assert needs_rehash(legacy_hash) is True

    fresh_hash = await hash_password(PASSWORD)
    assert needs_rehash(fresh_hash) is False


async def test_broken_hash_is_marked_for_rehash() -> None:
    """Мусор вместо хэша не роняет вход, а помечается на замену."""
    assert needs_rehash("not-a-valid-argon2-hash") is True
