"""HEL-980: one screen share at a time, with one waiting request."""

import json

import pytest
from sqlalchemy import select

from app.audit.models import AuditRecord
from app.meetings.models import MeetingShare
from app.meetings.sharing import BASE_SOURCES, SHARE_TOPIC, SHARING_SOURCES
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, A (700), B (701) and C (702) 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", "702"):
        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", track_source=None):
    body = {
        "event": event,
        "room": room,
        "identity": identity,
        "connection_id": connection,
        "track_source": track_source,
    }
    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 _share(client, headers, meeting_id, device=DEVICE):
    return await client.post(
        f"/v1/meetings/{meeting_id}/share", headers=headers, json={"device_id": device}
    )


async def _state(client, headers, meeting_id):
    return (await client.get(f"/v1/meetings/{meeting_id}/share", headers=headers)).json()


def _who(state, key):
    return state[key]["user_id"] if state[key] else None


def _last_broadcast(fake_livekit):
    return [sent for sent in fake_livekit.sent if sent.topic == SHARE_TOPIC][-1]


# --- Sharing and waiting ---------------------------------------------------


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

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

    assert (response.status_code, response.json()) == (200, {"status": "sharing"})
    assert fake_livekit.sources[(meeting_id, _identity("700"))] == SHARING_SOURCES
    sent = _last_broadcast(fake_livekit)
    assert sent.identities is None  # everyone
    assert sent.message == {
        "presenter": {"identity": _identity("700"), "user_id": "700", "name": "Person 700"},
        "waiting": None,
    }
    state = await _state(client, host, meeting_id)
    assert (_who(state, "presenter"), _who(state, "waiting")) == ("700", None)


async def test_second_person_waits_and_everyone_is_told(client, host, meeting, fake_livekit):
    # AC-08.7: A sees a request with Stop sharing; the hosts see it too.
    meeting_id, people = meeting
    await _share(client, people["700"], meeting_id)

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

    assert (response.status_code, response.json()) == (202, {"status": "waiting"})
    assert (meeting_id, _identity("701")) not in fake_livekit.sources  # can't publish yet
    message = _last_broadcast(fake_livekit).message
    assert (message["presenter"]["user_id"], message["waiting"]["user_id"]) == ("700", "701")


async def test_only_one_request_waits(client, host, meeting):
    # AC-08.12 / BR-08.7
    meeting_id, people = meeting
    await _share(client, people["700"], meeting_id)
    await _share(client, people["701"], meeting_id)

    response = await _share(client, people["702"], meeting_id)

    assert response.status_code == 409
    assert response.json()["error"] == {
        "code": "share_request_waiting",
        "message": "Someone is already waiting to share.",
    }
    state = await _state(client, host, meeting_id)
    assert _who(state, "waiting") == "701"


async def test_asking_again_changes_nothing(client, host, meeting):
    meeting_id, people = meeting
    await _share(client, people["700"], meeting_id)
    await _share(client, people["701"], meeting_id)

    again = await _share(client, people["701"], meeting_id)
    presenter_again = await _share(client, people["700"], meeting_id)

    assert again.status_code == 202
    assert presenter_again.status_code == 200
    state = await _state(client, host, meeting_id)
    assert (_who(state, "presenter"), _who(state, "waiting")) == ("700", "701")


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

    response = await _share(client, people["700"], meeting_id, device="not-connected-1")

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


# --- The share ends: the waiting one goes live -----------------------------


async def _a_sharing_b_waiting(client, people, meeting_id):
    await _share(client, people["700"], meeting_id)
    await _share(client, people["701"], meeting_id)


async def test_presenter_stops_and_the_waiting_one_goes_live(client, host, meeting, fake_livekit):
    # AC-08.8
    meeting_id, people = meeting
    await _a_sharing_b_waiting(client, people, meeting_id)

    response = await client.post(f"/v1/meetings/{meeting_id}/share/stop", headers=people["700"])

    assert response.status_code == 204
    assert fake_livekit.sources[(meeting_id, _identity("700"))] == BASE_SOURCES
    assert fake_livekit.sources[(meeting_id, _identity("701"))] == SHARING_SOURCES
    state = await _state(client, host, meeting_id)
    assert (_who(state, "presenter"), _who(state, "waiting")) == ("701", None)
    assert _last_broadcast(fake_livekit).message["presenter"]["identity"] == _identity("701")


async def test_presenter_stops_with_nobody_waiting(client, host, meeting, fake_livekit):
    meeting_id, people = meeting
    await _share(client, people["700"], meeting_id)

    await client.post(f"/v1/meetings/{meeting_id}/share/stop", headers=people["700"])

    assert await _state(client, host, meeting_id) == {"presenter": None, "waiting": None}
    assert fake_livekit.sources[(meeting_id, _identity("700"))] == BASE_SOURCES


async def test_host_stops_any_share_and_the_waiting_one_goes_live(
    client, db, host, meeting, fake_livekit
):
    # AC-08.9; decided 3 Nov: any end of A's share lets B go live.
    meeting_id, people = meeting
    await _a_sharing_b_waiting(client, people, meeting_id)

    response = await client.post(f"/v1/meetings/{meeting_id}/share/stop-presenter", headers=host)

    assert response.status_code == 204
    assert fake_livekit.sources[(meeting_id, _identity("700"))] == BASE_SOURCES
    state = await _state(client, host, meeting_id)
    assert _who(state, "presenter") == "701"
    db.expire_all()
    [record] = await db.scalars(
        select(AuditRecord).where(AuditRecord.action == "share_stopped_by_host")
    )
    assert (record.actor_user_id, record.details) == (
        HOST,
        {"user_id": "700", "name": "Person 700"},
    )


async def test_co_host_can_stop_a_share(client, host, meeting):
    meeting_id, people = meeting
    await client.post(f"/v1/meetings/{meeting_id}/co-hosts/702", headers=host)
    await _share(client, people["700"], meeting_id)

    response = await client.post(
        f"/v1/meetings/{meeting_id}/share/stop-presenter", headers=people["702"]
    )

    assert response.status_code == 204


async def test_participants_cannot_stop_someone_elses_share(client, host, meeting):
    # AC-08.10
    meeting_id, people = meeting
    await _share(client, people["700"], meeting_id)

    as_host = await client.post(
        f"/v1/meetings/{meeting_id}/share/stop-presenter", headers=people["701"]
    )
    as_presenter = await client.post(f"/v1/meetings/{meeting_id}/share/stop", headers=people["701"])

    assert as_host.status_code == 403
    assert as_presenter.status_code == 403
    assert _who(await _state(client, host, meeting_id), "presenter") == "700"


async def test_waiting_person_cancels(client, host, meeting, fake_livekit):
    # AC-08.13
    meeting_id, people = meeting
    await _a_sharing_b_waiting(client, people, meeting_id)

    response = await client.delete(
        f"/v1/meetings/{meeting_id}/share/request", headers=people["701"]
    )

    assert response.status_code == 204
    state = await _state(client, host, meeting_id)
    assert (_who(state, "presenter"), _who(state, "waiting")) == ("700", None)
    assert _last_broadcast(fake_livekit).message["waiting"] is None


async def test_presenter_dropping_lets_the_waiting_one_go_live(client, host, meeting, fake_livekit):
    meeting_id, people = meeting
    await _a_sharing_b_waiting(client, people, meeting_id)

    await _webhook(client, "participant_left", meeting_id, _identity("700"))

    assert _who(await _state(client, host, meeting_id), "presenter") == "701"
    assert fake_livekit.sources[(meeting_id, _identity("701"))] == SHARING_SOURCES


async def test_presenter_stopping_from_the_browser_lets_the_waiting_one_go_live(
    client, host, meeting
):
    # The browser's own "Stop sharing" bar: LiveKit reports the screen track gone.
    meeting_id, people = meeting
    await _a_sharing_b_waiting(client, people, meeting_id)

    await _webhook(
        client, "track_unpublished", meeting_id, _identity("700"), track_source="SCREEN_SHARE"
    )

    assert _who(await _state(client, host, meeting_id), "presenter") == "701"


async def test_camera_off_is_not_a_share_ending(client, host, meeting):
    meeting_id, people = meeting
    await _share(client, people["700"], meeting_id)

    await _webhook(client, "track_unpublished", meeting_id, _identity("700"), track_source="CAMERA")

    assert _who(await _state(client, host, meeting_id), "presenter") == "700"


async def test_waiting_person_dropping_closes_the_request(client, host, meeting):
    meeting_id, people = meeting
    await _a_sharing_b_waiting(client, people, meeting_id)

    await _webhook(client, "participant_left", meeting_id, _identity("701"))

    state = await _state(client, host, meeting_id)
    assert (_who(state, "presenter"), _who(state, "waiting")) == ("700", None)


async def test_meeting_end_closes_everything(client, db, host, meeting):
    # AC-08.13: ...or the meeting ends.
    meeting_id, people = meeting
    await _a_sharing_b_waiting(client, people, meeting_id)

    await client.post(f"/v1/meetings/{meeting_id}/end", headers=host)

    db.expire_all()
    assert await db.get(MeetingShare, meeting_id) is None
