"""HEL-983 (Mute, Mute all) and HEL-984 (Remove)."""

import json

import pytest
from sqlalchemy import select, update

from app.audit.models import AuditRecord
from app.meetings.controls import MUTED_TOPIC
from app.meetings.models import MeetingMember, MemberRole
from app.meetings.places import count_places
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"
PHONE = "phone-device-02"


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, fake_livekit):
    """A running meeting: the Host, 700, 701 and 702 connected, every microphone on."""
    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))
    fake_livekit.mics_on.add((meeting_id, _identity(HOST, HOST_DEVICE)))
    people = {}
    for user in ("700", "701", "702"):
        people[user] = await _in_meeting(client, host, meeting_id, user)
        fake_livekit.mics_on.add((meeting_id, _identity(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 _admit(client, host, meeting_id, user):
    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


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
    await _admit(client, host, meeting_id, user)
    assert (await _join(client, person, meeting_id)).status_code == 200
    await _webhook(client, "participant_joined", meeting_id, _identity(user))
    return person


async def _second_device(client, fake_livekit, meeting_id, person, user):
    assert (await _join(client, person, meeting_id, PHONE)).status_code == 200
    await _webhook(client, "participant_joined", meeting_id, _identity(user, PHONE), "conn-2")
    fake_livekit.mics_on.add((meeting_id, _identity(user, PHONE)))


async def _co_host(client, host, meeting_id, user):
    response = await client.post(f"/v1/meetings/{meeting_id}/co-hosts/{user}", headers=host)
    assert response.status_code == 204


async def _mute(client, headers, meeting_id, user):
    return await client.post(f"/v1/meetings/{meeting_id}/participants/{user}/mute", headers=headers)


async def _mute_all(client, headers, meeting_id):
    return await client.post(f"/v1/meetings/{meeting_id}/mute-all", headers=headers)


async def _remove(client, headers, meeting_id, user):
    return await client.post(
        f"/v1/meetings/{meeting_id}/participants/{user}/remove", headers=headers
    )


async def _records(db, action):
    return list(await db.scalars(select(AuditRecord).where(AuditRecord.action == action)))


def _muted_users(fake_livekit):
    return sorted({identity.split(":")[2] for _, identity in fake_livekit.muted})


# --- Mute one ------------------------------------------------------------------


async def test_host_mutes_someone_on_all_their_devices_and_they_are_told(
    client, db, meeting, host, fake_livekit
):
    # AC-11.1
    meeting_id, people = meeting
    await _second_device(client, fake_livekit, meeting_id, people["700"], "700")

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

    assert response.status_code == 204
    both = sorted([(meeting_id, _identity("700")), (meeting_id, _identity("700", PHONE))])
    assert sorted(fake_livekit.muted) == both
    [told] = [sent for sent in fake_livekit.sent if sent.topic == MUTED_TOPIC]
    assert sorted(told.identities) == [identity for _, identity in both]
    assert told.message == {"by_user_id": HOST, "by_name": "Test User"}
    [record] = await _records(db, "participant_muted")
    assert (record.actor_user_id, record.details) == (
        HOST,
        {"user_id": "700", "name": "Person 700"},
    )


async def test_a_microphone_already_off_is_left_alone(client, db, meeting, host, fake_livekit):
    meeting_id, _ = meeting
    fake_livekit.mics_on.discard((meeting_id, _identity("700")))

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

    assert response.status_code == 204
    assert fake_livekit.muted == []
    assert [sent for sent in fake_livekit.sent if sent.topic == MUTED_TOPIC] == []
    assert await _records(db, "participant_muted") == []


async def test_participant_cannot_mute_anyone(client, db, meeting, fake_livekit):
    # AC-11.4 / BR-11.1
    meeting_id, people = meeting

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

    assert response.status_code == 403
    assert response.json()["error"]["code"] == "not_a_host"
    assert fake_livekit.muted == []


async def test_co_host_mutes_a_participant(client, meeting, host, fake_livekit):
    meeting_id, people = meeting
    await _co_host(client, host, meeting_id, "700")

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


async def test_nobody_mutes_the_host(client, meeting, host, fake_livekit):
    meeting_id, people = meeting
    await _co_host(client, host, meeting_id, "700")

    response = await _mute(client, people["700"], meeting_id, HOST)

    assert response.status_code == 403
    assert response.json()["error"]["code"] == "not_allowed"
    assert fake_livekit.muted == []


async def test_only_the_host_mutes_a_co_host(client, meeting, host, fake_livekit):
    meeting_id, people = meeting
    await _co_host(client, host, meeting_id, "700")
    await _co_host(client, host, meeting_id, "701")

    refused = await _mute(client, people["700"], meeting_id, "701")
    by_host = await _mute(client, host, meeting_id, "701")

    assert refused.json()["error"]["code"] == "not_allowed"
    assert by_host.status_code == 204
    assert _muted_users(fake_livekit) == ["701"]


async def test_muting_someone_not_in_the_meeting(client, meeting, host):
    meeting_id, _ = meeting

    response = await _mute(client, host, meeting_id, "799")

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


async def test_mute_when_livekit_is_down(client, meeting, host, fake_livekit):
    meeting_id, _ = meeting
    fake_livekit.healthy = False

    assert (await _mute(client, host, meeting_id, "700")).status_code == 503


# --- Mute all ------------------------------------------------------------------


@pytest.mark.parametrize("caller", ["host", "co_host"])
async def test_mute_all_spares_the_caller_the_host_and_co_hosts(
    client, db, meeting, host, fake_livekit, caller
):
    # AC-11.2, AC-11.6, BR-11.3
    meeting_id, people = meeting
    await _co_host(client, host, meeting_id, "700")
    selecting = host if caller == "host" else people["700"]

    response = await _mute_all(client, selecting, meeting_id)

    assert response.status_code == 200
    assert response.json() == {"muted": 2}
    assert _muted_users(fake_livekit) == ["701", "702"]
    [told] = [sent for sent in fake_livekit.sent if sent.topic == MUTED_TOPIC]
    assert sorted(told.identities) == [_identity("701"), _identity("702")]
    [record] = await _records(db, "all_muted")
    assert record.details == {"people": 2}


async def test_acting_host_selecting_mute_all_is_not_muted(client, db, meeting, fake_livekit):
    # BR-11.3: never the person who selects it (an Acting Host isn't spared by role).
    meeting_id, people = meeting
    await db.execute(
        update(MeetingMember)
        .where(MeetingMember.meeting_id == meeting_id, MeetingMember.user_id == "700")
        .values(role=MemberRole.ACTING_HOST)
    )
    await db.commit()

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

    assert response.json() == {"muted": 2}
    assert _muted_users(fake_livekit) == ["701", "702"]


async def test_mute_all_is_one_time(client, meeting, host, fake_livekit):
    # AC-11.5 / BR-11.4: someone joining afterwards keeps their microphone.
    meeting_id, _ = meeting
    await _mute_all(client, host, meeting_id)
    await _in_meeting(client, host, meeting_id, "703")
    fake_livekit.mics_on.add((meeting_id, _identity("703")))

    assert "703" not in _muted_users(fake_livekit)
    assert (meeting_id, _identity("703")) in fake_livekit.mics_on


async def test_mute_all_with_nobody_to_mute(client, db, meeting, host, fake_livekit):
    meeting_id, _ = meeting
    fake_livekit.mics_on = {(meeting_id, _identity(HOST, HOST_DEVICE))}

    response = await _mute_all(client, host, meeting_id)

    assert response.json() == {"muted": 0}
    assert await _records(db, "all_muted") == []


async def test_participant_cannot_mute_all(client, meeting, fake_livekit):
    meeting_id, people = meeting

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

    assert response.status_code == 403
    assert response.json()["error"]["code"] == "not_a_host"
    assert fake_livekit.muted == []


# --- Remove --------------------------------------------------------------------


async def test_host_removes_someone_from_all_their_devices(client, db, meeting, host, fake_livekit):
    # AC-12.1: disconnected (their screen gets LiveKit's "removed"); the record says who.
    meeting_id, people = meeting
    await _second_device(client, fake_livekit, meeting_id, people["700"], "700")

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

    assert response.status_code == 204
    assert sorted(fake_livekit.removed) == sorted(
        [(meeting_id, _identity("700")), (meeting_id, _identity("700", PHONE))]
    )
    [record] = await _records(db, "participant_removed")
    assert (record.actor_user_id, record.details) == (
        HOST,
        {"user_id": "700", "name": "Person 700", "reason": "removed_by_host"},
    )


async def test_removed_person_waits_for_a_fresh_admission(client, meeting, host):
    # AC-12.2, AC-12.4, BR-12.1: admitted moments ago, yet they can't walk back in.
    meeting_id, people = meeting
    await _remove(client, host, meeting_id, "700")

    asking = await _join(client, people["700"], meeting_id)
    assert asking.status_code == 202

    await _admit(client, host, meeting_id, "700")
    back = await _join(client, people["700"], meeting_id)
    assert (back.status_code, back.json()["microphone_on"]) == (200, False)


async def test_removing_frees_their_place(client, db, meeting, host):
    # Even before LiveKit's "left" arrives: their place no longer counts.
    meeting_id, _ = meeting
    assert (await count_places(db, meeting_id)).in_use() == 4

    await _remove(client, host, meeting_id, "700")

    db.expire_all()
    assert (await count_places(db, meeting_id)).in_use() == 3


async def test_participant_cannot_remove_anyone(client, db, meeting, fake_livekit):
    # AC-12.3
    meeting_id, people = meeting

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

    assert response.status_code == 403
    assert response.json()["error"]["code"] == "not_a_host"
    assert fake_livekit.removed == []
    assert await _records(db, "participant_removed") == []


@pytest.mark.parametrize("target", [HOST, "701"])
async def test_co_host_cannot_remove_the_host_or_another_co_host(
    client, meeting, host, fake_livekit, target
):
    # BR-12.3; only the Host acts on Co-hosts (Arshdeep, 3 Nov).
    meeting_id, people = meeting
    await _co_host(client, host, meeting_id, "700")
    await _co_host(client, host, meeting_id, "701")

    response = await _remove(client, people["700"], meeting_id, target)

    assert response.status_code == 403
    assert response.json()["error"]["code"] == "not_allowed"
    assert fake_livekit.removed == []


async def test_co_host_removes_a_participant(client, meeting, host, fake_livekit):
    meeting_id, people = meeting
    await _co_host(client, host, meeting_id, "700")

    assert (await _remove(client, people["700"], meeting_id, "701")).status_code == 204
    assert fake_livekit.removed == [(meeting_id, _identity("701"))]


async def test_removed_co_host_loses_the_role_for_this_meeting(client, meeting, host):
    meeting_id, people = meeting
    await _co_host(client, host, meeting_id, "700")

    assert (await _remove(client, host, meeting_id, "700")).status_code == 204

    assert (await _join(client, people["700"], meeting_id)).status_code == 202
    await _admit(client, host, meeting_id, "700")
    back = await _join(client, people["700"], meeting_id)
    assert back.json()["role"] == "participant"


async def test_host_cannot_remove_themselves(client, meeting, host):
    meeting_id, _ = meeting

    response = await _remove(client, host, meeting_id, HOST)

    assert response.json()["error"]["code"] == "not_allowed"


@pytest.mark.parametrize("who", ["never_joined", "waiting", "already_removed"])
async def test_removing_someone_not_in_the_meeting(client, meeting, host, who):
    meeting_id, _ = meeting
    target = "700"
    if who == "never_joined":
        target = "799"
    elif who == "waiting":
        target = "710"
        waiting = await session_headers(client, sub=target, name="Waiting")
        assert (await _join(client, waiting, meeting_id)).status_code == 202
    else:
        await _remove(client, host, meeting_id, target)

    response = await _remove(client, host, meeting_id, target)

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


async def test_remove_when_livekit_is_down_saves_nothing(client, db, meeting, host, fake_livekit):
    meeting_id, people = meeting
    fake_livekit.healthy = False

    assert (await _remove(client, host, meeting_id, "700")).status_code == 503

    fake_livekit.healthy = True
    assert await _records(db, "participant_removed") == []
    # Still in: back straight away.
    assert (await _join(client, people["700"], meeting_id, PHONE)).status_code == 200
