"""HEL-1012: who manages after an unexpected drop (Acting Host)."""

import json

import pytest
from sqlalchemy import select

from app.audit.models import AuditRecord
from app.meetings.managers import MANAGER_TOPIC
from app.meetings.models import Meeting, MeetingMember, MeetingStatus
from app.meetings.rooms import WAITING_TOPIC
from app.revocation.recheck import recheck_once
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 app(app_client):
    return app_client[0]


@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_id(client, host):
    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))
    return meeting_id


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, meeting_id, identity=None, connection="conn-1"):
    body = {"event": event, "room": meeting_id, "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):
    """A colleague waits, is admitted, joins and connects."""
    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 _drop(client, meeting_id, user, device=DEVICE, connection="conn-1"):
    """The device's connection ends without Leave meeting (tab closed, network lost)."""
    await _webhook(client, "participant_left", meeting_id, _identity(user, device), connection)


async def _role(db, meeting_id, user):
    db.expire_all()
    return (await db.get(MeetingMember, (meeting_id, user))).role


def _notices(fake_livekit):
    return [sent for sent in fake_livekit.sent if sent.topic == MANAGER_TOPIC]


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


async def _make_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


# --- A Co-host is there ----------------------------------------------------


async def test_host_drops_and_the_co_host_carries_on(client, db, host, meeting_id, fake_livekit):
    # AC-22.1
    await _in_meeting(client, host, meeting_id, "700")
    await _make_co_host(client, host, meeting_id, "700")

    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    assert await _role(db, meeting_id, HOST) == "host"  # still the Host
    [notice] = _notices(fake_livekit)
    assert notice.identities is None  # everyone
    assert notice.message == {
        "reason": "host_disconnected",
        "managers": [{"user_id": "700", "name": "Person 700"}],
        "acting_host": False,
    }
    assert await _records(db, "acting_host_appointed") == []


async def test_several_co_hosts_are_all_named(client, db, host, meeting_id, fake_livekit):
    # BR-22.4
    for user in ("700", "701"):
        await _in_meeting(client, host, meeting_id, user)
        await _make_co_host(client, host, meeting_id, user)

    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    [notice] = _notices(fake_livekit)
    assert sorted(m["user_id"] for m in notice.message["managers"]) == ["700", "701"]


async def test_co_host_dropping_while_the_host_is_there_changes_nothing(
    client, db, host, meeting_id, fake_livekit
):
    await _in_meeting(client, host, meeting_id, "700")
    await _make_co_host(client, host, meeting_id, "700")

    await _drop(client, meeting_id, "700")

    assert _notices(fake_livekit) == []


async def test_host_still_on_another_device_is_not_a_drop(
    client, db, host, meeting_id, fake_livekit
):
    await _in_meeting(client, host, meeting_id, "700")
    await _join(client, host, meeting_id, "host-phone-02")
    await _webhook(client, "participant_joined", meeting_id, _identity(HOST, "host-phone-02"))

    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    assert await _role(db, meeting_id, "700") == "participant"
    assert _notices(fake_livekit) == []


# --- The last one managing drops -------------------------------------------


async def test_person_present_longest_becomes_acting_host(
    client, db, host, meeting_id, fake_livekit
):
    # AC-22.2
    await _in_meeting(client, host, meeting_id, "700")
    await _in_meeting(client, host, meeting_id, "701")

    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    assert await _role(db, meeting_id, "700") == "acting_host"
    assert await _role(db, meeting_id, "701") == "participant"
    assert await _role(db, meeting_id, HOST) == "host"
    assert fake_livekit.attributes[(meeting_id, _identity("700"))] == {"role": "acting_host"}
    [notice] = _notices(fake_livekit)
    assert notice.message == {
        "reason": "host_disconnected",
        "managers": [{"user_id": "700", "name": "Person 700"}],
        "acting_host": True,
    }
    [record] = await _records(db, "acting_host_appointed")
    assert (record.actor_user_id, record.details) == (
        "system",
        {"user_id": "700", "name": "Person 700", "after": HOST},
    )


async def test_present_longest_counts_the_current_connection(client, db, host, meeting_id):
    # BR-22.2: 700 joined first, but dropped and came back after 701.
    person = await _in_meeting(client, host, meeting_id, "700")
    await _in_meeting(client, host, meeting_id, "701")
    await _drop(client, meeting_id, "700")
    await _join(client, person, meeting_id)
    await _webhook(client, "participant_joined", meeting_id, _identity("700"), "conn-2")

    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    assert await _role(db, meeting_id, "701") == "acting_host"


async def test_acting_host_gets_the_waiting_alerts_and_can_admit(
    client, db, host, meeting_id, fake_livekit
):
    # AC-22.5, BR-22.3
    acting = await _in_meeting(client, host, meeting_id, "700")
    waiting = await session_headers(client, sub="702", name="Person 702")
    await _join(client, waiting, meeting_id)

    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    alert = [sent for sent in fake_livekit.sent if sent.topic == WAITING_TOPIC][-1]
    assert alert.identities == [_identity("700")]
    assert alert.message == {"waiting": 1}
    listing = (await client.get(f"/v1/meetings/{meeting_id}/waiting", headers=acting)).json()
    request_id = listing["waiting"][0]["request_id"]
    admit = f"/v1/meetings/{meeting_id}/waiting/{request_id}/admit"
    assert (await client.post(admit, headers=acting)).status_code == 204


async def test_acting_host_dropping_passes_it_on(client, db, host, meeting_id, fake_livekit):
    # AC-22.3
    await _in_meeting(client, host, meeting_id, "700")
    await _in_meeting(client, host, meeting_id, "701")
    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    await _drop(client, meeting_id, "700")

    assert await _role(db, meeting_id, "701") == "acting_host"
    assert await _role(db, meeting_id, "700") == "participant"
    assert _notices(fake_livekit)[-1].message["managers"] == [
        {"user_id": "701", "name": "Person 701"}
    ]


async def test_nobody_eligible_means_no_acting_host(client, db, host, meeting_id, fake_livekit):
    # AC-22.4: only someone waiting; the 2-minute auto-end (HEL-1026) ends it.
    await _join(client, await session_headers(client, sub="702"), meeting_id)

    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    assert _notices(fake_livekit) == []
    assert await _records(db, "acting_host_appointed") == []
    db.expire_all()
    assert (await db.get(Meeting, meeting_id)).status == MeetingStatus.ACTIVE


async def test_leaving_on_purpose_never_makes_an_acting_host(
    client, db, host, meeting_id, fake_livekit
):
    # AC-22.8 / BR-22.5: Leave, then LiveKit reports the device gone.
    await _in_meeting(client, host, meeting_id, "700")
    await _make_co_host(client, host, meeting_id, "700")
    await client.post(
        f"/v1/meetings/{meeting_id}/leave", headers=host, json={"device_id": HOST_DEVICE}
    )

    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    assert _notices(fake_livekit) == []


async def test_host_removed_by_the_re_check_is_a_drop(
    app, client, db, host, meeting_id, fake_helixtahr
):
    await _in_meeting(client, host, meeting_id, "700")
    fake_helixtahr.revoked_users.add(HOST)

    state = app.state
    await recheck_once(
        state.db_sessions, state.livekit, state.products, state.sessions, state.alerts
    )

    assert await _role(db, meeting_id, "700") == "acting_host"


# --- Coming back -----------------------------------------------------------


async def test_host_coming_back_gets_their_rights_back(client, db, host, meeting_id, fake_livekit):
    # AC-22.7
    await _in_meeting(client, host, meeting_id, "700")
    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    rejoin = await _join(client, host, meeting_id, HOST_DEVICE)
    await _webhook(client, "participant_joined", meeting_id, _identity(HOST, HOST_DEVICE), "c2")

    assert rejoin.json()["role"] == "host"
    assert await _role(db, meeting_id, "700") == "participant"
    assert fake_livekit.attributes[(meeting_id, _identity("700"))] == {"role": "participant"}
    assert _notices(fake_livekit)[-1].message == {
        "reason": "host_back",
        "managers": [{"user_id": HOST, "name": "Test User"}],
        "acting_host": False,
    }
    [record] = await _records(db, "acting_host_ended")
    assert record.details == {"user_id": "700", "back": HOST}


async def test_co_host_coming_back_also_takes_over(client, db, host, meeting_id):
    # Decided 3 Nov: an Acting Host only while no Host or Co-host is there.
    co_host = await _in_meeting(client, host, meeting_id, "700")
    await _make_co_host(client, host, meeting_id, "700")
    await _in_meeting(client, host, meeting_id, "701")
    await _drop(client, meeting_id, HOST, HOST_DEVICE)
    await _drop(client, meeting_id, "700")
    assert await _role(db, meeting_id, "701") == "acting_host"

    await _join(client, co_host, meeting_id)
    await _webhook(client, "participant_joined", meeting_id, _identity("700"), "c2")

    assert await _role(db, meeting_id, "701") == "participant"
    assert await _role(db, meeting_id, "700") == "co_host"


# --- What an Acting Host may do --------------------------------------------


async def test_acting_host_can_end_but_not_make_co_hosts(client, db, host, meeting_id):
    # BR-22.3
    acting = await _in_meeting(client, host, meeting_id, "700")
    await _in_meeting(client, host, meeting_id, "701")
    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    make = await client.post(f"/v1/meetings/{meeting_id}/co-hosts/701", headers=acting)
    end = await client.post(f"/v1/meetings/{meeting_id}/end", headers=acting)

    assert make.status_code == 403
    assert end.status_code == 204


async def test_acting_host_leaving_passes_on_the_acting_role_only(
    client, db, host, meeting_id, fake_livekit
):
    # BR-24.9, AC-24.14; Arshdeep 3 Nov: the Host stays Host.
    acting = await _in_meeting(client, host, meeting_id, "700")
    await _in_meeting(client, host, meeting_id, "701")
    await _drop(client, meeting_id, HOST, HOST_DEVICE)
    leave = f"/v1/meetings/{meeting_id}/leave"

    first = await client.post(leave, headers=acting, json={"device_id": DEVICE})
    second = await client.post(
        leave, headers=acting, json={"device_id": DEVICE, "new_host_user_id": "701"}
    )

    assert first.json()["error"]["code"] == "handover_required"
    assert second.json() == {"status": "handed_over", "new_host_name": "Person 701"}
    assert await _role(db, meeting_id, "701") == "acting_host"
    assert await _role(db, meeting_id, "700") == "participant"
    db.expire_all()
    assert (await db.get(Meeting, meeting_id)).host_user_id == HOST
    assert _notices(fake_livekit)[-1].message["reason"] == "handed_over"


async def test_acting_host_joining_another_device_is_acting_host(client, db, host, meeting_id):
    acting = await _in_meeting(client, host, meeting_id, "700")
    await _drop(client, meeting_id, HOST, HOST_DEVICE)

    response = await _join(client, acting, meeting_id, "phone-device-2")

    assert (response.status_code, response.json()["role"]) == (200, "acting_host")
