"""
Test utils functions
"""

# pylint: disable=W0621
import json
from unittest import mock

from django.conf import settings
from django.contrib.auth.models import AnonymousUser

import jwt
import pytest
from livekit.api import TwirpError

from core.factories import UserFactory
from core.utils import (
    NotificationError,
    create_livekit_client,
    generate_token,
    notify_participants,
)

pytestmark = pytest.mark.django_db


def decode_token(token: str) -> dict:
    """Decode a LiveKit JWT access token for inspection."""
    return jwt.decode(
        token,
        settings.LIVEKIT_CONFIGURATION["api_secret"],
        algorithms=["HS256"],
    )


def test_generate_token_authenticated_uses_full_name():
    """The token's display name should default to the user's full name."""
    user = UserFactory(full_name="Jane Doe")

    token = generate_token(room="my-room", user=user)

    claims = decode_token(token)
    assert claims["name"] == "Jane Doe"
    assert claims["sub"] == str(user.sub)


def test_generate_token_authenticated_fallback_user_representation():
    """
    When the user has no full name, the token's display name should fall back
    to the user's string representation.
    """
    user = UserFactory(full_name=None)

    token = generate_token(room="my-room", user=user)

    claims = decode_token(token)
    assert claims["name"] == str(user)


def test_generate_token_explicit_username_overrides_default():
    """An explicitly provided username should take precedence over the full name."""
    user = UserFactory(full_name="Jane Doe")

    token = generate_token(room="my-room", user=user, username="Custom Name")

    claims = decode_token(token)
    assert claims["name"] == "Custom Name"


def test_authenticated_username_ignored_when_editing_disabled(settings):
    """With editing disabled, an authenticated user's username is ignored."""
    settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
    user = UserFactory(full_name="Jane Doe")
    token = generate_token(room="my-room", user=user, username="Custom Name")
    claims = decode_token(token)
    assert claims["name"] == "Jane Doe"


def test_authenticated_default_name_unaffected_when_editing_disabled(settings):
    """Disabling editing doesn't disturb the default full-name path."""
    settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
    user = UserFactory(full_name="Jane Doe")
    token = generate_token(room="my-room", user=user)
    claims = decode_token(token)
    assert claims["name"] == "Jane Doe"


def test_anonymous_uses_username_when_provided():
    """An anonymous user's provided username is used as the display name."""
    token = generate_token(room="my-room", user=AnonymousUser(), username="Guest42")
    claims = decode_token(token)
    assert claims["name"] == "Guest42"


def test_anonymous_username_used_even_when_editing_disabled(settings):
    """The setting governs authenticated users only; anonymous can still set a name."""
    settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
    token = generate_token(room="my-room", user=AnonymousUser(), username="Guest42")
    claims = decode_token(token)
    assert claims["name"] == "Guest42"


def test_anonymous_falls_back_to_anonymous_label():
    """With no username, an anonymous user is labelled 'Anonymous'."""
    token = generate_token(room="my-room", user=AnonymousUser())
    claims = decode_token(token)
    assert claims["name"] == "Anonymous"


@mock.patch("asyncio.get_running_loop")
@mock.patch("core.utils.LiveKitAPI")
def test_create_livekit_client_ssl_enabled(
    mock_livekit_api, mock_get_running_loop, settings
):
    """Test LiveKitAPI client creation with SSL verification enabled."""
    mock_get_running_loop.return_value = mock.MagicMock()
    settings.LIVEKIT_VERIFY_SSL = True

    create_livekit_client()

    mock_livekit_api.assert_called_once_with(
        **settings.LIVEKIT_CONFIGURATION, session=None
    )


@mock.patch("core.utils.aiohttp.ClientSession")
@mock.patch("asyncio.get_running_loop")
@mock.patch("core.utils.LiveKitAPI")
def test_create_livekit_client_ssl_disabled(
    mock_livekit_api, mock_get_running_loop, mock_client_session, settings
):
    """Test LiveKitAPI client creation with SSL verification disabled."""
    mock_get_running_loop.return_value = mock.MagicMock()
    mock_session_instance = mock.MagicMock()
    mock_client_session.return_value = mock_session_instance
    settings.LIVEKIT_VERIFY_SSL = False

    create_livekit_client()

    mock_livekit_api.assert_called_once_with(
        **settings.LIVEKIT_CONFIGURATION, session=mock_session_instance
    )


@mock.patch("asyncio.get_running_loop")
@mock.patch("core.utils.LiveKitAPI")
def test_create_livekit_client_custom_configuration(
    mock_livekit_api, mock_get_running_loop, settings
):
    """Test LiveKitAPI client creation with custom configuration."""
    settings.LIVEKIT_VERIFY_SSL = True

    mock_get_running_loop.return_value = mock.MagicMock()
    custom_configuration = {
        "api_key": "mock_key",
        "api_secret": "mock_secret",
        "url": "http://mock-url.com",
    }

    create_livekit_client(custom_configuration)

    mock_livekit_api.assert_called_once_with(**custom_configuration, session=None)


@mock.patch("core.utils.create_livekit_client")
def test_notify_participants_error(mock_create_livekit_client):
    """Test participant notification with API error."""

    # Set up the mock LiveKitAPI and its behavior
    mock_api_instance = mock.Mock()
    mock_api_instance.room = mock.Mock()
    mock_api_instance.room.send_data = mock.AsyncMock(
        side_effect=TwirpError(msg="test error", code=123, status=123)
    )

    class MockResponse:
        """LiveKit API response mock with non-empty rooms list."""

        rooms = ["room-1"]

    mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())

    mock_api_instance.aclose = mock.AsyncMock()
    mock_create_livekit_client.return_value = mock_api_instance

    # Call the function and expect an exception
    with pytest.raises(NotificationError, match="Failed to notify room participants"):
        notify_participants(room_name="room-number-1", notification_data={"foo": "foo"})

    # Verify that the service checked for existing rooms
    mock_api_instance.room.list_rooms.assert_called_once()

    # Verify send_data was called
    mock_api_instance.room.send_data.assert_called_once()

    # Verify aclose was still called after the exception
    mock_api_instance.aclose.assert_called_once()


@mock.patch("core.utils.create_livekit_client")
def test_notify_participants_success_no_room(mock_create_livekit_client):
    """Test the notify_participants function when the LiveKit room doesn't exist."""

    # Set up the mock LiveKitAPI and its behavior
    mock_api_instance = mock.Mock()
    mock_api_instance.room = mock.Mock()
    mock_api_instance.room.send_data = mock.AsyncMock()

    # Create a proper response object with an empty rooms list
    class MockResponse:
        """LiveKit API response mock with empty rooms list."""

        rooms = []

    mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
    mock_api_instance.aclose = mock.AsyncMock()
    mock_create_livekit_client.return_value = mock_api_instance

    notify_participants(room_name="room-number-1", notification_data={"foo": "foo"})

    # Verify that the service checked for existing rooms
    mock_api_instance.room.list_rooms.assert_called_once()

    # Verify the send_data method was not called since no room exists
    mock_api_instance.room.send_data.assert_not_called()

    # Verify the connection was properly closed
    mock_api_instance.aclose.assert_called_once()


@mock.patch("core.utils.create_livekit_client")
def test_notify_participants_success(mock_create_livekit_client):
    """Test successful participant notification."""

    # Set up the mock LiveKitAPI and its behavior
    mock_api_instance = mock.Mock()
    mock_api_instance.room = mock.Mock()
    mock_api_instance.room.send_data = mock.AsyncMock()

    class MockResponse:
        """LiveKit API response mock with non-empty rooms list."""

        rooms = ["room-1"]

    mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())

    mock_api_instance.aclose = mock.AsyncMock()
    mock_create_livekit_client.return_value = mock_api_instance

    # Call the function
    notify_participants(room_name="room-number-1", notification_data={"foo": "foo"})

    # Verify that the service checked for existing rooms
    mock_api_instance.room.list_rooms.assert_called_once()

    # Verify the send_data method was called
    mock_api_instance.room.send_data.assert_called_once()
    send_data_request = mock_api_instance.room.send_data.call_args[0][0]
    assert send_data_request.room == "room-number-1"
    assert json.loads(send_data_request.data.decode("utf-8")) == {"foo": "foo"}
    assert send_data_request.kind == 0  # RELIABLE mode in Livekit protocol

    # Verify aclose was called
    mock_api_instance.aclose.assert_called_once()
