"""HEL-1032: raised hands (reactions are LiveKit data messages: nothing in the backend)."""

import json
from datetime import UTC, datetime, timedelta

import pytest
from sqlalchemy import func, select

from app.audit.models import AuditRecord
from app.meetings.hands import HAND_ATTRIBUTE, HAND_LOWERED_TOPIC
from tests.conftest import make_app, running, session_headers
from tests.fakes import FAKE_WEBHOOK_SIGNATURE

HOST = "674"
HOST_DEVICE = "host-laptop-01"
DEVICE = "tab-device-01"


def _identity(user: str, device: str = DEVICE) -> str:
    return f"helixtahr:26:{user}:{device}"


@pytest.fixture
async def app_client(db, fake_livekit, fake_helixtahr):
    app = make_app(livekit=fake_livekit, helixtahr=fake_helixtahr)
    async with running(app) as client:
        yield app, client


@pytest.fixture
def client(app_client):
    return app_client[1]


@pytest.fixture
async def host(client):
    return await session_headers(client)


@pytest.fixture
async def meeting(client, host):
    """A running meeting: the Host, 700 and 701 connected."""
    meeting_id = (await client.post("/v1/meetings", headers=host)).json()["id"]
    assert (await _join(client, host, meeting_id, HOST_DEVICE)).status_code == 200
    await _webhook(client, "participant_joined", meeting_id, _identity(HOST, HOST_DEVICE))
    people = {}
    for user in ("700", "701"):
        people[user] = await _in_meeting(client, host, meeting_id, user)
    return meeting_id, people


async def _join(client, headers, meeting_id, device=DEVICE):
    return await client.post(
        f"/v1/meetings/{meeting_id}/join", headers=headers, json={"device_id": device}
    )


async def _webhook(client, event, room, identity=None, connection="conn-1"):
    body = {"event": event, "room": room, "identity": identity, "connection_id": connection}
    return await client.post(
        "/livekit/webhook",
        content=json.dumps(body),
        headers={"Authorization": FAKE_WEBHOOK_SIGNATURE},
    )


async def _in_meeting(client, host, meeting_id, user):
    person = await session_headers(client, sub=user, name=f"Person {user}")
    assert (await _join(client, person, meeting_id)).status_code == 202
    waiting = (await client.get(f"/v1/meetings/{meeting_id}/waiting", headers=host)).json()
    request_id = next(r["request_id"] for r in waiting["waiting"] if r["user_id"] == user)
    admit = f"/v1/meetings/{meeting_id}/waiting/{request_id}/admit"
    assert (await client.post(admit, headers=host)).status_code == 204
    assert (await _join(client, person, meeting_id)).status_code == 200
    await _webhook(client, "participant_joined", meeting_id, _identity(user))
    return person


async def _hand(client, headers, meeting_id, raised, device=DEVICE):
    return await client.post(
        f"/v1/meetings/{meeting_id}/hand",
        headers=headers,
        json={"device_id": device, "raised": raised},
    )


async def _lower(client, headers, meeting_id, user):
    return await client.post(f"/v1/meetings/{meeting_id}/hands/{user}/lower", headers=headers)


def _hand_of(fake_livekit, meeting_id, identity):
    return fake_livekit.attributes.get((meeting_id, identity), {}).get(HAND_ATTRIBUTE)


async def test_raising_a_hand_sets_when_it_went_up(client, meeting, fake_livekit):
    # AC-28.1: everyone's People panel sees the attribute (earliest first).
    meeting_id, people = meeting
    sent_before = len(fake_livekit.sent)

    response = await _hand(client, people["700"], meeting_id, raised=True)

    assert response.status_code == 204
    raised_at = datetime.fromisoformat(_hand_of(fake_livekit, meeting_id, _identity("700")))
    assert abs(datetime.now(UTC) - raised_at) < timedelta(seconds=5)
    # AC-28.6 / BR-28.4: no pop-up, no message to anyone.
    assert len(fake_livekit.sent) == sent_before


async def test_lowering_my_own_hand(client, meeting, fake_livekit):
    meeting_id, people = meeting
    await _hand(client, people["700"], meeting_id, raised=True)

    await _hand(client, people["700"], meeting_id, raised=False)

    assert _hand_of(fake_livekit, meeting_id, _identity("700")) == ""  # removed in LiveKit


async def test_host_lowers_someones_hand_on_all_their_devices(client, meeting, host, fake_livekit):
    # AC-28.3
    meeting_id, people = meeting
    await _join(client, people["700"], meeting_id, "phone-device-2")
    await _webhook(client, "participant_joined", meeting_id, _identity("700", "phone-device-2"))
    await _hand(client, people["700"], meeting_id, raised=True)

    response = await _lower(client, host, meeting_id, "700")

    assert response.status_code == 204
    assert _hand_of(fake_livekit, meeting_id, _identity("700")) == ""
    assert _hand_of(fake_livekit, meeting_id, _identity("700", "phone-device-2")) == ""
    [told] = [sent for sent in fake_livekit.sent if sent.topic == HAND_LOWERED_TOPIC]
    assert sorted(told.identities) == sorted([_identity("700"), _identity("700", "phone-device-2")])
    assert told.message == {"by_user_id": HOST, "by_name": "Test User"}


async def test_co_host_can_lower_a_hand(client, meeting, host):
    meeting_id, people = meeting
    await client.post(f"/v1/meetings/{meeting_id}/co-hosts/701", headers=host)
    await _hand(client, people["700"], meeting_id, raised=True)

    assert (await _lower(client, people["701"], meeting_id, "700")).status_code == 204


async def test_participants_cannot_lower_someone_elses_hand(client, meeting, fake_livekit):
    # AC-28.7
    meeting_id, people = meeting
    await _hand(client, people["700"], meeting_id, raised=True)

    response = await _lower(client, people["701"], meeting_id, "700")

    assert response.status_code == 403
    assert response.json()["error"]["code"] == "not_a_host"
    assert _hand_of(fake_livekit, meeting_id, _identity("700")) != ""


async def test_device_must_be_in_the_meeting(client, meeting):
    meeting_id, people = meeting

    response = await _hand(client, people["700"], meeting_id, raised=True, device="elsewhere-01")

    assert response.status_code == 409
    assert response.json()["error"]["code"] == "not_in_meeting"


async def test_livekit_down(client, meeting, fake_livekit):
    meeting_id, people = meeting
    fake_livekit.healthy = False

    response = await _hand(client, people["700"], meeting_id, raised=True)

    assert response.status_code == 503


async def test_nothing_about_hands_is_stored(client, db, meeting, host):
    # BR-28.3: no table holds hands; not even an audit record.
    meeting_id, people = meeting
    before = await db.scalar(select(func.count()).select_from(AuditRecord))
    await _hand(client, people["700"], meeting_id, raised=True)
    await _lower(client, host, meeting_id, "700")

    db.expire_all()
    assert await db.scalar(select(func.count()).select_from(AuditRecord)) == before
