"""Tests for gRPC mixin helper functions.

Tests UUID parsing, feature requirements, service requirements, and get-or-abort helpers.
"""

from __future__ import annotations

from collections.abc import Awaitable, Callable
from unittest.mock import AsyncMock, MagicMock
from uuid import UUID

import pytest

from noteflow.domain.ports.unit_of_work import UnitOfWork
from noteflow.domain.value_objects import MeetingId
from noteflow.grpc._mixins.errors import (
    AbortableContext,
    get_meeting_or_abort,
    get_project_or_abort,
    get_webhook_or_abort,
    parse_entity_id,
    parse_integration_id,
    parse_meeting_id,
    parse_project_id,
    parse_webhook_id,
    parse_workspace_id,
    require_feature_entities,
    require_feature_integrations,
    require_feature_projects,
    require_feature_webhooks,
    require_feature_workspaces,
    require_ner_service,
    require_project_service,
)

# Valid UUID for testing
VALID_UUID = "12345678-1234-5678-1234-567812345678"

# Type aliases
ParseHelper = Callable[[str, AbortableContext], Awaitable[UUID]]
FeatureHelper = Callable[[UnitOfWork, AbortableContext], Awaitable[None]]


@pytest.fixture
def mock_context() -> AsyncMock:
    """Create mock gRPC context that raises on abort."""
    context = AsyncMock()
    context.abort = AsyncMock(side_effect=Exception("Aborted"))
    return context


@pytest.fixture
def mock_uow_all_features() -> MagicMock:
    """Create mock UoW with all features supported."""
    uow = MagicMock()
    uow.supports_projects = True
    uow.supports_webhooks = True
    uow.supports_entities = True
    uow.supports_integrations = True
    uow.supports_workspaces = True
    uow.meetings = AsyncMock()
    return uow


@pytest.fixture
def mock_uow_no_features() -> MagicMock:
    """Create mock UoW with no features supported."""
    uow = MagicMock()
    uow.supports_projects = False
    uow.supports_webhooks = False
    uow.supports_entities = False
    uow.supports_integrations = False
    uow.supports_workspaces = False
    return uow


class TestParseUuidHelpers:
    """Tests for UUID parsing helper functions."""

    @pytest.mark.parametrize(
        "helper",
        [
            parse_workspace_id,
            parse_project_id,
            parse_meeting_id,
            parse_integration_id,
            parse_webhook_id,
            parse_entity_id,
        ],
    )
    async def test_valid_uuid_returns_uuid(
        self,
        helper: ParseHelper,
        mock_context: AsyncMock,
    ) -> None:
        """Parse helpers return UUID for valid input."""
        result = await helper(VALID_UUID, mock_context)
        assert result == UUID(VALID_UUID)
        mock_context.abort.assert_not_called()

    @pytest.mark.parametrize(
        "helper",
        [
            parse_workspace_id,
            parse_project_id,
            parse_meeting_id,
            parse_integration_id,
            parse_webhook_id,
            parse_entity_id,
        ],
    )
    async def test_invalid_uuid_aborts(
        self,
        helper: ParseHelper,
        mock_context: AsyncMock,
    ) -> None:
        """Parse helpers abort for invalid UUID format."""
        with pytest.raises(Exception, match="Aborted"):
            await helper("not-a-uuid", mock_context)
        mock_context.abort.assert_called_once()


class TestFeatureRequirements:
    """Tests for feature requirement helper functions."""

    @pytest.mark.parametrize(
        "helper",
        [
            require_feature_projects,
            require_feature_webhooks,
            require_feature_entities,
            require_feature_integrations,
            require_feature_workspaces,
        ],
    )
    async def test_feature_supported_passes(
        self,
        helper: FeatureHelper,
        mock_context: AsyncMock,
        mock_uow_all_features: MagicMock,
    ) -> None:
        """Feature requirement passes when feature is supported."""
        await helper(mock_uow_all_features, mock_context)
        mock_context.abort.assert_not_called()

    @pytest.mark.parametrize(
        "helper",
        [
            require_feature_projects,
            require_feature_webhooks,
            require_feature_entities,
            require_feature_integrations,
            require_feature_workspaces,
        ],
    )
    async def test_feature_unsupported_aborts(
        self,
        helper: FeatureHelper,
        mock_context: AsyncMock,
        mock_uow_no_features: MagicMock,
    ) -> None:
        """Feature requirement aborts when feature is not supported."""
        with pytest.raises(Exception, match="Aborted"):
            await helper(mock_uow_no_features, mock_context)
        mock_context.abort.assert_called_once()


class TestServiceRequirements:
    """Tests for service requirement helper functions."""

    async def test_require_project_service_returns_service(
        self,
        mock_context: AsyncMock,
    ) -> None:
        """Return project service when configured."""
        service = MagicMock()
        result = await require_project_service(service, mock_context)
        assert result is service
        mock_context.abort.assert_not_called()

    async def test_require_project_service_aborts_when_none(
        self,
        mock_context: AsyncMock,
    ) -> None:
        """Abort when project service is None."""
        with pytest.raises(Exception, match="Aborted"):
            await require_project_service(None, mock_context)
        mock_context.abort.assert_called_once()

    async def test_require_ner_service_returns_service(
        self,
        mock_context: AsyncMock,
    ) -> None:
        """Return NER service when configured."""
        service = MagicMock()
        result = await require_ner_service(service, mock_context)
        assert result is service
        mock_context.abort.assert_not_called()

    async def test_require_ner_service_aborts_when_none(
        self,
        mock_context: AsyncMock,
    ) -> None:
        """Abort when NER service is None."""
        with pytest.raises(Exception, match="Aborted"):
            await require_ner_service(None, mock_context)
        mock_context.abort.assert_called_once()


class TestGetOrAbortHelpers:
    """Tests for get-or-abort helper functions."""

    async def test_get_meeting_or_abort_returns_meeting(
        self,
        mock_context: AsyncMock,
        mock_uow_all_features: MagicMock,
    ) -> None:
        """Return meeting when found."""
        meeting = MagicMock()
        meeting_id = MeetingId(UUID(VALID_UUID))
        mock_uow_all_features.meetings.get = AsyncMock(return_value=meeting)

        result = await get_meeting_or_abort(mock_uow_all_features, meeting_id, mock_context)

        assert result is meeting
        mock_context.abort.assert_not_called()

    async def test_get_meeting_or_abort_aborts_when_not_found(
        self,
        mock_context: AsyncMock,
        mock_uow_all_features: MagicMock,
    ) -> None:
        """Abort with NOT_FOUND when meeting is None."""
        meeting_id = MeetingId(UUID(VALID_UUID))
        mock_uow_all_features.meetings.get = AsyncMock(return_value=None)

        with pytest.raises(Exception, match="Aborted"):
            await get_meeting_or_abort(mock_uow_all_features, meeting_id, mock_context)

        mock_context.abort.assert_called_once()

    async def test_get_project_or_abort_returns_project(
        self,
        mock_context: AsyncMock,
        mock_uow_all_features: MagicMock,
    ) -> None:
        """Return project when found via service."""
        project = MagicMock()
        project_id = UUID(VALID_UUID)
        project_service = MagicMock()
        project_service.get_project = AsyncMock(return_value=project)

        result = await get_project_or_abort(
            project_service, mock_uow_all_features, project_id, mock_context
        )

        assert result is project
        mock_context.abort.assert_not_called()

    async def test_get_project_or_abort_aborts_when_not_found(
        self,
        mock_context: AsyncMock,
        mock_uow_all_features: MagicMock,
    ) -> None:
        """Abort with NOT_FOUND when project is None."""
        project_id = UUID(VALID_UUID)
        project_service = MagicMock()
        project_service.get_project = AsyncMock(return_value=None)

        with pytest.raises(Exception, match="Aborted"):
            await get_project_or_abort(
                project_service, mock_uow_all_features, project_id, mock_context
            )

        mock_context.abort.assert_called_once()

    async def test_get_webhook_or_abort_returns_webhook(
        self,
        mock_context: AsyncMock,
        mock_uow_all_features: MagicMock,
    ) -> None:
        """Return webhook config when found."""
        webhook = MagicMock()
        webhook_id = UUID(VALID_UUID)
        mock_uow_all_features.webhooks = MagicMock()
        mock_uow_all_features.webhooks.get_by_id = AsyncMock(return_value=webhook)

        result = await get_webhook_or_abort(mock_uow_all_features, webhook_id, mock_context)

        assert result is webhook
        mock_context.abort.assert_not_called()

    async def test_get_webhook_or_abort_aborts_when_not_found(
        self,
        mock_context: AsyncMock,
        mock_uow_all_features: MagicMock,
    ) -> None:
        """Abort with NOT_FOUND when webhook is None."""
        webhook_id = UUID(VALID_UUID)
        mock_uow_all_features.webhooks = MagicMock()
        mock_uow_all_features.webhooks.get_by_id = AsyncMock(return_value=None)

        with pytest.raises(Exception, match="Aborted"):
            await get_webhook_or_abort(mock_uow_all_features, webhook_id, mock_context)

        mock_context.abort.assert_called_once()
