"""Tests for cloud consent gRPC endpoints.

Validates the GrantCloudConsent, RevokeCloudConsent, and GetCloudConsentStatus
RPCs work correctly with the summarization service.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, cast
from unittest.mock import AsyncMock, MagicMock

import pytest

from noteflow.application.services.summarization_service import (
    SummarizationService,
    SummarizationServiceSettings,
)
from noteflow.grpc._config import ServicesConfig
from noteflow.grpc.proto import noteflow_pb2
from noteflow.grpc.service import NoteFlowServicer

if TYPE_CHECKING:
    from collections.abc import Callable


class _DummyContext:
    """Minimal gRPC context for testing."""

    def __init__(self) -> None:
        self.aborted = False
        self.abort_code: object = None
        self.abort_details: str = ""

    async def abort(self, code: object, details: str) -> None:
        self.aborted = True
        self.abort_code = code
        self.abort_details = details
        raise AssertionError(f"abort called: {code} - {details}")


def _create_mocksummarization_service(
    *,
    initial_consent: bool = False,
    on_consent_change: Callable[[bool], None] | None = None,
) -> SummarizationService:
    """Create a mock summarization service with controllable consent state."""
    settings = SummarizationServiceSettings(cloud_consent_granted=initial_consent)
    service = MagicMock(spec=SummarizationService)
    service.settings = settings

    async def grant() -> None:
        settings.cloud_consent_granted = True
        if on_consent_change:
            on_consent_change(True)

    async def revoke() -> None:
        settings.cloud_consent_granted = False
        if on_consent_change:
            on_consent_change(False)

    service.grant_cloud_consent = AsyncMock(side_effect=grant)
    service.revoke_cloud_consent = AsyncMock(side_effect=revoke)
    service.cloud_consent_granted = property(lambda _: settings.cloud_consent_granted)

    # Make cloud_consent_granted work as a property
    type(service).cloud_consent_granted = property(
        lambda self: self.settings.cloud_consent_granted
    )

    return service


class TestGetCloudConsentStatus:
    """Tests for GetCloudConsentStatus RPC."""

    @pytest.mark.asyncio
    async def test_returns_false_when_consent_not_granted(self) -> None:
        """Status should be false when consent has not been granted."""
        service = _create_mocksummarization_service(initial_consent=False)
        servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))

        response = await servicer.GetCloudConsentStatus(
            noteflow_pb2.GetCloudConsentStatusRequest(),
            _DummyContext(),
        )

        assert response.consent_granted is False, "consent should be False when not granted"

    @pytest.mark.asyncio
    async def test_returns_true_when_consent_granted(self) -> None:
        """Status should be true when consent has been granted."""
        service = _create_mocksummarization_service(initial_consent=True)
        servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))

        response = await servicer.GetCloudConsentStatus(
            noteflow_pb2.GetCloudConsentStatusRequest(),
            _DummyContext(),
        )

        assert response.consent_granted is True, "consent should be True when granted"

    @pytest.mark.asyncio
    async def test_returns_false_when_service_unavailable(self) -> None:
        """Should return false (safe default) when service not configured."""
        servicer = NoteFlowServicer()

        response = await servicer.GetCloudConsentStatus(
            noteflow_pb2.GetCloudConsentStatusRequest(),
            _DummyContext(),
        )

        # Safe default: no consent when service unavailable
        assert response.consent_granted is False, "should default to False when service unavailable"


class TestGrantCloudConsent:
    """Tests for GrantCloudConsent RPC."""

    @pytest.mark.asyncio
    async def test_grants_consent(self) -> None:
        """Granting consent should update the service state."""
        service = _create_mocksummarization_service(initial_consent=False)
        servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))

        response = await servicer.GrantCloudConsent(
            noteflow_pb2.GrantCloudConsentRequest(),
            _DummyContext(),
        )

        assert isinstance(response, noteflow_pb2.GrantCloudConsentResponse), (
            "GrantCloudConsent should return GrantCloudConsentResponse"
        )
        grant_mock = cast(AsyncMock, service.grant_cloud_consent)
        grant_mock.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_grant_is_idempotent(self) -> None:
        """Granting consent multiple times should not error."""
        service = _create_mocksummarization_service(initial_consent=True)
        servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))

        # Grant when already granted
        response = await servicer.GrantCloudConsent(
            noteflow_pb2.GrantCloudConsentRequest(),
            _DummyContext(),
        )

        assert isinstance(response, noteflow_pb2.GrantCloudConsentResponse), (
            "GrantCloudConsent should return GrantCloudConsentResponse even when already granted"
        )

    @pytest.mark.asyncio
    async def test_grant_aborts_when_service_unavailable(self) -> None:
        """Should abort with FAILED_PRECONDITION when service not configured."""
        servicer = NoteFlowServicer()
        context = _DummyContext()

        with pytest.raises(AssertionError, match="abort called"):
            await servicer.GrantCloudConsent(
                noteflow_pb2.GrantCloudConsentRequest(),
                context,
            )

        assert context.aborted, "should abort when service unavailable"


class TestRevokeCloudConsent:
    """Tests for RevokeCloudConsent RPC."""

    @pytest.mark.asyncio
    async def test_revokes_consent(self) -> None:
        """Revoking consent should update the service state."""
        service = _create_mocksummarization_service(initial_consent=True)
        servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))

        response = await servicer.RevokeCloudConsent(
            noteflow_pb2.RevokeCloudConsentRequest(),
            _DummyContext(),
        )

        assert isinstance(response, noteflow_pb2.RevokeCloudConsentResponse), (
            "RevokeCloudConsent should return RevokeCloudConsentResponse"
        )
        revoke_mock = cast(AsyncMock, service.revoke_cloud_consent)
        revoke_mock.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_revoke_is_idempotent(self) -> None:
        """Revoking consent when not granted should not error."""
        service = _create_mocksummarization_service(initial_consent=False)
        servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))

        response = await servicer.RevokeCloudConsent(
            noteflow_pb2.RevokeCloudConsentRequest(),
            _DummyContext(),
        )

        assert isinstance(response, noteflow_pb2.RevokeCloudConsentResponse), (
            "RevokeCloudConsent should return RevokeCloudConsentResponse even when not granted"
        )

    @pytest.mark.asyncio
    async def test_revoke_aborts_when_service_unavailable(self) -> None:
        """Should abort with FAILED_PRECONDITION when service not configured."""
        servicer = NoteFlowServicer()
        context = _DummyContext()

        with pytest.raises(AssertionError, match="abort called"):
            await servicer.RevokeCloudConsent(
                noteflow_pb2.RevokeCloudConsentRequest(),
                context,
            )

        assert context.aborted, "should abort when service unavailable"


class TestConsentRoundTrip:
    """Integration tests for consent grant/revoke/status cycle."""

    @pytest.mark.asyncio
    async def test_grant_then_check_status(self) -> None:
        """Granting consent should be reflected in status check."""
        service = _create_mocksummarization_service(initial_consent=False)
        servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))
        context = _DummyContext()

        # Verify initial state
        status_before = await servicer.GetCloudConsentStatus(
            noteflow_pb2.GetCloudConsentStatusRequest(),
            context,
        )
        assert status_before.consent_granted is False, "initial consent should be False"

        # Grant consent
        await servicer.GrantCloudConsent(
            noteflow_pb2.GrantCloudConsentRequest(),
            _DummyContext(),
        )

        # Verify new state
        status_after = await servicer.GetCloudConsentStatus(
            noteflow_pb2.GetCloudConsentStatusRequest(),
            _DummyContext(),
        )
        assert status_after.consent_granted is True, "consent should be True after grant"

    @pytest.mark.asyncio
    async def test_grant_revoke_cycle(self) -> None:
        """Full grant/revoke cycle should work correctly."""
        service = _create_mocksummarization_service(initial_consent=False)
        servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))

        # Grant
        await servicer.GrantCloudConsent(
            noteflow_pb2.GrantCloudConsentRequest(),
            _DummyContext(),
        )
        status = await servicer.GetCloudConsentStatus(
            noteflow_pb2.GetCloudConsentStatusRequest(),
            _DummyContext(),
        )
        assert status.consent_granted is True, "consent should be True after grant"

        # Revoke
        await servicer.RevokeCloudConsent(
            noteflow_pb2.RevokeCloudConsentRequest(),
            _DummyContext(),
        )
        status = await servicer.GetCloudConsentStatus(
            noteflow_pb2.GetCloudConsentStatusRequest(),
            _DummyContext(),
        )
        assert status.consent_granted is False, "consent should be False after revoke"

    @pytest.mark.asyncio
    async def test_consent_change_callback_invoked(self) -> None:
        """Consent changes should invoke the on_consent_change callback."""
        callback_values: list[bool] = []

        def track_changes(granted: bool) -> None:
            callback_values.append(granted)

        service = _create_mocksummarization_service(
            initial_consent=False,
            on_consent_change=track_changes,
        )
        servicer = NoteFlowServicer(services=ServicesConfig(summarization_service=service))

        await servicer.GrantCloudConsent(
            noteflow_pb2.GrantCloudConsentRequest(),
            _DummyContext(),
        )
        await servicer.RevokeCloudConsent(
            noteflow_pb2.RevokeCloudConsentRequest(),
            _DummyContext(),
        )

        assert callback_values == [True, False], (
            "on_consent_change callback should receive [True, False] for grant then revoke"
        )
