"""Тесты очереди поднятых рук поверх WS комнаты (`WS /api/v1/conferences/{id}/chat`, задача B1).

Протокол и аутентификация — общие с чатом (`api/chat.py`), поэтому структура
тестов и хелперы намеренно зеркалят `tests/test_chat_ws.py`.
"""

import uuid
from collections.abc import AsyncGenerator, Callable
from typing import Any

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

from core.security import create_access_token, hash_password
from models.conference import Conference
from models.guest import GuestAccess
from models.user import User
from services.conference_ids import generate_number, generate_slug
from services.instance_settings import InstanceSettingsService, SettingsUpdateIn
from services.livekit_tokens import create_room_access_token
from tests.conftest import ASGIWebSocketSession

WSFactory = Callable[[str], ASGIWebSocketSession]


@pytest_asyncio.fixture(autouse=True)
async def _isolated_instance_settings(clean_instance_settings: None) -> AsyncGenerator[None, None]:
    """Autouse только в этом модуле — см. `tests.test_chat_ws._isolated_instance_settings`
    и `tests.conftest.clean_instance_settings` (почему не глобальный autouse)."""
    yield


# --- Хелперы (см. tests/test_chat_ws.py) ------------------------------------


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


async def _make_conference(
    session: AsyncSession, *, owner_id: uuid.UUID | None = None, status: str = "active"
) -> Conference:
    conference = Conference(
        number=generate_number(),
        slug=generate_slug(),
        title="Hand Queue Test",
        status=status,
        owner_id=owner_id,
    )
    session.add(conference)
    await session.flush()
    return conference


async def _make_guest(session: AsyncSession, conference: Conference, *, name: str) -> GuestAccess:
    guest = GuestAccess(conference_id=conference.id, display_name=name)
    session.add(guest)
    await session.flush()
    return guest


def _chat_path(conference_id: uuid.UUID) -> str:
    return f"/api/v1/conferences/{conference_id}/chat"


def _user_token(conference: Conference, user: User) -> str:
    return create_room_access_token(
        room_name=conference.slug, identity=str(user.id), name=user.name_user
    )


def _guest_token(conference: Conference, guest: GuestAccess) -> str:
    return create_room_access_token(
        room_name=conference.slug, identity=f"guest:{guest.id}", name=guest.display_name
    )


async def _connect_auth_and_queue(
    session: ASGIWebSocketSession, token: str
) -> dict[str, Any]:
    """Подключиться, аутентифицироваться, вычитать `history` и вернуть снапшот очереди."""
    accept = await session.connect()
    assert accept["type"] == "websocket.accept"
    await session.send_json({"type": "auth", "token": token})
    await session.receive_json()  # history — не интересен этим тестам
    return await session.receive_json()


def _identities(queue_frame: dict[str, Any]) -> list[str]:
    return [entry["identity"] for entry in queue_frame["queue"]]


# --- Поднять/опустить свою руку -----------------------------------------------


async def test_raise_and_lower_own_hand_broadcasts_to_everyone(
    db_session: AsyncSession, ws_client: WSFactory
) -> None:
    conference = await _make_conference(db_session)
    alice = await _make_user(db_session, name="Alice")
    bob = await _make_user(db_session, name="Bob")
    await db_session.commit()

    path = _chat_path(conference.id)
    ws1 = ws_client(path)
    await _connect_auth_and_queue(ws1, _user_token(conference, alice))
    ws2 = ws_client(path)
    initial2 = await _connect_auth_and_queue(ws2, _user_token(conference, bob))
    assert initial2 == {"type": "hand_queue", "queue": []}

    await ws1.send_json({"type": "raise_hand"})

    queue1 = await ws1.receive_json()
    assert _identities(queue1) == [str(alice.id)]
    assert queue1["queue"][0]["name"] == "Alice"
    assert queue1["queue"][0]["raised_at"].endswith("Z")

    queue2 = await ws2.receive_json()
    assert queue2 == queue1

    await ws1.send_json({"type": "lower_hand"})
    queue1_after = await ws1.receive_json()
    assert queue1_after == {"type": "hand_queue", "queue": []}
    queue2_after = await ws2.receive_json()
    assert queue2_after == queue1_after


async def test_raise_hand_order_is_preserved(
    db_session: AsyncSession, ws_client: WSFactory
) -> None:
    """Порядок в очереди — по времени поднятия, не по алфавиту/подключению."""
    conference = await _make_conference(db_session)
    alice = await _make_user(db_session, name="Alice")
    bob = await _make_user(db_session, name="Bob")
    await db_session.commit()

    path = _chat_path(conference.id)
    ws1 = ws_client(path)
    await _connect_auth_and_queue(ws1, _user_token(conference, alice))
    ws2 = ws_client(path)
    await _connect_auth_and_queue(ws2, _user_token(conference, bob))

    # Боб поднимает руку ПЕРВЫМ, хотя подключился вторым — он и должен
    # оказаться первым в очереди.
    await ws2.send_json({"type": "raise_hand"})
    await ws2.receive_json()
    await ws1.receive_json()

    await ws1.send_json({"type": "raise_hand"})
    queue = await ws1.receive_json()

    assert _identities(queue) == [str(bob.id), str(alice.id)]


async def test_re_raising_hand_does_not_move_position(
    db_session: AsyncSession, ws_client: WSFactory
) -> None:
    """Повторное поднятие уже поднятой руки — идемпотентно, место в очереди не меняется."""
    conference = await _make_conference(db_session)
    alice = await _make_user(db_session, name="Alice")
    bob = await _make_user(db_session, name="Bob")
    await db_session.commit()

    path = _chat_path(conference.id)
    ws1 = ws_client(path)
    await _connect_auth_and_queue(ws1, _user_token(conference, alice))
    ws2 = ws_client(path)
    await _connect_auth_and_queue(ws2, _user_token(conference, bob))

    await ws1.send_json({"type": "raise_hand"})
    first = await ws1.receive_json()
    await ws2.receive_json()

    await ws2.send_json({"type": "raise_hand"})
    await ws2.receive_json()
    await ws1.receive_json()

    # Алиса (уже в очереди первой) поднимает руку ещё раз.
    await ws1.send_json({"type": "raise_hand"})
    repeated = await ws1.receive_json()
    await ws2.receive_json()

    assert _identities(repeated) == [str(alice.id), str(bob.id)]
    assert repeated["queue"][0]["raised_at"] == first["queue"][0]["raised_at"]


async def test_guest_can_raise_hand(db_session: AsyncSession, ws_client: WSFactory) -> None:
    conference = await _make_conference(db_session)
    guest = await _make_guest(db_session, conference, name="Guest Carl")
    await db_session.commit()

    ws = ws_client(_chat_path(conference.id))
    await _connect_auth_and_queue(ws, _guest_token(conference, guest))

    await ws.send_json({"type": "raise_hand"})
    queue = await ws.receive_json()
    assert _identities(queue) == [f"guest:{guest.id}"]
    assert queue["queue"][0]["name"] == "Guest Carl"


# --- Права организатора -------------------------------------------------------


async def test_non_organizer_cannot_lower_someone_elses_hand(
    db_session: AsyncSession, ws_client: WSFactory
) -> None:
    owner = await _make_user(db_session, name="Owner")
    conference = await _make_conference(db_session, owner_id=owner.id)
    alice = await _make_user(db_session, name="Alice")
    bob = await _make_user(db_session, name="Bob")
    await db_session.commit()

    path = _chat_path(conference.id)
    ws1 = ws_client(path)
    await _connect_auth_and_queue(ws1, _user_token(conference, alice))
    ws2 = ws_client(path)
    await _connect_auth_and_queue(ws2, _user_token(conference, bob))

    await ws1.send_json({"type": "raise_hand"})
    await ws1.receive_json()
    await ws2.receive_json()

    # Боб (обычный участник, не организатор) пытается опустить руку Алисы.
    await ws2.send_json({"type": "lower_hand", "identity": str(alice.id)})
    error = await ws2.receive_json()
    assert error == {"type": "error", "code": "forbidden"}


async def test_organizer_can_lower_someone_elses_hand(
    db_session: AsyncSession, ws_client: WSFactory
) -> None:
    owner = await _make_user(db_session, name="Owner")
    conference = await _make_conference(db_session, owner_id=owner.id)
    alice = await _make_user(db_session, name="Alice")
    await db_session.commit()

    path = _chat_path(conference.id)
    ws_alice = ws_client(path)
    await _connect_auth_and_queue(ws_alice, _user_token(conference, alice))
    ws_owner = ws_client(path)
    await _connect_auth_and_queue(ws_owner, _user_token(conference, owner))

    await ws_alice.send_json({"type": "raise_hand"})
    await ws_alice.receive_json()
    await ws_owner.receive_json()

    await ws_owner.send_json({"type": "lower_hand", "identity": str(alice.id)})
    queue_owner = await ws_owner.receive_json()
    queue_alice = await ws_alice.receive_json()

    assert queue_owner == {"type": "hand_queue", "queue": []}
    assert queue_alice == queue_owner


async def test_organizer_joining_late_sees_already_raised_hands(
    db_session: AsyncSession, ws_client: WSFactory
) -> None:
    """Организатор зашёл позже, когда руки уже подняты, — видит актуальную очередь сразу."""
    owner = await _make_user(db_session, name="Owner")
    conference = await _make_conference(db_session, owner_id=owner.id)
    alice = await _make_user(db_session, name="Alice")
    await db_session.commit()

    path = _chat_path(conference.id)
    ws_alice = ws_client(path)
    await _connect_auth_and_queue(ws_alice, _user_token(conference, alice))
    await ws_alice.send_json({"type": "raise_hand"})
    await ws_alice.receive_json()

    ws_owner = ws_client(path)
    initial_queue = await _connect_auth_and_queue(ws_owner, _user_token(conference, owner))

    assert _identities(initial_queue) == [str(alice.id)]


# --- Отключаемый модуль (`instance_settings.hand_queue`) ---------------------


async def test_hand_queue_disabled_rejects_raise_and_lower(
    db_session: AsyncSession, ws_client: WSFactory
) -> None:
    """Выключенный модуль — вторая линия защиты сверх фронта: `raise_hand`/
    `lower_hand` отклоняются кодом `hand_queue_disabled`, очередь не меняется."""
    conference = await _make_conference(db_session)
    alice = await _make_user(db_session, name="Alice")
    await db_session.commit()

    await InstanceSettingsService(db_session).update(SettingsUpdateIn(hand_queue_enabled=False))

    ws = ws_client(_chat_path(conference.id))
    await _connect_auth_and_queue(ws, _user_token(conference, alice))

    await ws.send_json({"type": "raise_hand"})
    error = await ws.receive_json()
    assert error == {"type": "error", "code": "hand_queue_disabled"}

    await ws.send_json({"type": "lower_hand"})
    error = await ws.receive_json()
    assert error == {"type": "error", "code": "hand_queue_disabled"}


async def test_join_out_reflects_hand_queue_enabled_toggle(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    user = await _make_user(db_session, name="Toggle Tester")
    await db_session.commit()
    headers = {"Authorization": f"Bearer {create_access_token(user.id, user.role)}"}

    response = await client.post(
        "/api/v1/conferences", json={"title": "Standup"}, headers=headers
    )
    assert response.status_code == 201, response.text
    assert response.json()["join"]["hand_queue_enabled"] is True

    await InstanceSettingsService(db_session).update(SettingsUpdateIn(hand_queue_enabled=False))

    response = await client.post(
        "/api/v1/conferences", json={"title": "Standup 2"}, headers=headers
    )
    assert response.status_code == 201, response.text
    assert response.json()["join"]["hand_queue_enabled"] is False


async def test_guest_join_out_reflects_hand_queue_enabled(
    client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
    conference = await _make_conference(db_session)
    await db_session.commit()

    await InstanceSettingsService(db_session).update(SettingsUpdateIn(hand_queue_enabled=False))

    response = await client.post(
        f"/api/v1/conferences/{conference.id}/guest-join", json={"display_name": "Dave"}
    )
    assert response.status_code == 200, response.text
    assert response.json()["hand_queue_enabled"] is False
