from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

import pytest

from api.routes.webrtc_signaling import SignalingManager
from api.services.call_concurrency import CallConcurrencyLimitError


class _FakeWebSocket:
    def __init__(self):
        self.send_json = AsyncMock()


class _FakePeerConnection:
    def __init__(self):
        self.renegotiate = AsyncMock()

    def get_answer(self):
        return {"sdp": "v=0\r\n", "type": "answer", "pc_id": "pc-1"}


def _offer_payload(pc_id: str = "pc-1") -> dict:
    return {
        "pc_id": pc_id,
        "sdp": "v=0\r\n",
        "type": "offer",
    }


@pytest.mark.asyncio
async def test_public_embed_offer_rejects_when_org_concurrency_limit_reached():
    manager = SignalingManager()
    ws = _FakeWebSocket()
    user = SimpleNamespace(id=7)

    with (
        patch("api.routes.webrtc_signaling.db_client") as mock_db,
        patch(
            "api.routes.webrtc_signaling.authorize_workflow_run_start",
            new=AsyncMock(
                return_value=SimpleNamespace(has_quota=True, error_message="")
            ),
        ),
        patch("api.routes.webrtc_signaling.call_concurrency") as mock_concurrency,
    ):
        mock_db.get_workflow_organization_id = AsyncMock(return_value=11)
        # A live run: this test is about the concurrency limit, not about the
        # spent-run refusal that precedes it.
        mock_db.get_workflow_run = AsyncMock(
            return_value=SimpleNamespace(id=501, is_completed=False)
        )
        mock_concurrency.acquire_org_slot = AsyncMock(
            side_effect=CallConcurrencyLimitError(
                organization_id=11,
                source="public_embed",
                wait_time=0,
                max_concurrent=2,
            )
        )
        mock_concurrency.bind_workflow_run = AsyncMock()

        await manager._handle_offer(
            ws,
            _offer_payload(),
            workflow_id=33,
            workflow_run_id=501,
            user=user,
            organization_id=11,
            connection_key="conn-1",
            enforce_call_concurrency=True,
            call_concurrency_source="public_embed",
        )

    ws.send_json.assert_awaited_once_with(
        {
            "type": "error",
            "payload": {
                "error_type": "concurrency_limit_exceeded",
                "message": "Concurrent call limit reached",
            },
        }
    )
    mock_concurrency.bind_workflow_run.assert_not_called()


@pytest.mark.asyncio
async def test_public_embed_renegotiation_does_not_acquire_another_slot():
    manager = SignalingManager()
    ws = _FakeWebSocket()
    user = SimpleNamespace(id=7)
    connection_key = "conn-1"
    pc = _FakePeerConnection()
    manager._peer_connections["pc-1"] = pc
    manager._peer_connection_owners["pc-1"] = connection_key

    with (
        patch("api.routes.webrtc_signaling.db_client") as mock_db,
        patch(
            "api.routes.webrtc_signaling.authorize_workflow_run_start",
            new=AsyncMock(
                return_value=SimpleNamespace(has_quota=True, error_message="")
            ),
        ),
        patch("api.routes.webrtc_signaling.call_concurrency") as mock_concurrency,
    ):
        mock_db.get_workflow_organization_id = AsyncMock(return_value=11)
        mock_concurrency.acquire_org_slot = AsyncMock()

        await manager._handle_offer(
            ws,
            _offer_payload(),
            workflow_id=33,
            workflow_run_id=501,
            user=user,
            organization_id=11,
            connection_key=connection_key,
            enforce_call_concurrency=True,
            call_concurrency_source="public_embed",
        )

    mock_concurrency.acquire_org_slot.assert_not_called()
    pc.renegotiate.assert_awaited_once()
    assert ws.send_json.await_args.args[0]["type"] == "answer"


@pytest.mark.asyncio
async def test_signaling_websocket_rejects_run_not_owned_by_workflow():
    """The URL workflow_id drives org/quota/concurrency accounting, so a
    workflow_id that doesn't own the run must be rejected before signaling."""
    from fastapi import HTTPException

    from api.routes.webrtc_signaling import signaling_websocket

    ws = _FakeWebSocket()
    user = SimpleNamespace(id=7, selected_organization_id=11)

    with (
        patch("api.routes.webrtc_signaling.db_client") as mock_db,
        patch("api.routes.webrtc_signaling.signaling_manager") as mock_manager,
    ):
        mock_db.get_workflow_run = AsyncMock(
            return_value=SimpleNamespace(id=501, workflow_id=99)
        )
        mock_manager.handle_websocket = AsyncMock()

        with pytest.raises(HTTPException) as exc_info:
            await signaling_websocket(
                ws, workflow_id=33, workflow_run_id=501, user=user
            )

    assert exc_info.value.status_code == 400
    mock_manager.handle_websocket.assert_not_called()


@pytest.mark.asyncio
async def test_signaling_websocket_accepts_matching_workflow_and_run():
    from api.routes.webrtc_signaling import signaling_websocket

    ws = _FakeWebSocket()
    user = SimpleNamespace(id=7, selected_organization_id=11)

    with (
        patch("api.routes.webrtc_signaling.db_client") as mock_db,
        patch("api.routes.webrtc_signaling.signaling_manager") as mock_manager,
    ):
        mock_db.get_workflow_run = AsyncMock(
            return_value=SimpleNamespace(id=501, workflow_id=33)
        )
        mock_manager.handle_websocket = AsyncMock()

        await signaling_websocket(ws, workflow_id=33, workflow_run_id=501, user=user)

    mock_manager.handle_websocket.assert_awaited_once()
