"""Tests for OAuth gRPC endpoints.

Validates the InitiateOAuth, CompleteOAuth, GetOAuthConnectionStatus, and
DisconnectOAuth RPCs work correctly with the calendar service.
"""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock

import pytest

from noteflow.application.services.calendar_service import CalendarServiceError
from noteflow.domain.entities.integration import IntegrationStatus
from noteflow.grpc._config import ServicesConfig
from noteflow.grpc.proto import noteflow_pb2
from noteflow.grpc.service import NoteFlowServicer

if TYPE_CHECKING:
    from noteflow.domain.ports.calendar import OAuthConnectionInfo


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_mock_connection_info(
    *,
    provider: str = "google",
    status: str = "disconnected",
    email: str | None = None,
    expires_at: datetime | None = None,
    error_message: str | None = None,
) -> OAuthConnectionInfo:
    """Create a mock OAuthConnectionInfo object."""
    info = MagicMock()
    info.provider = provider
    info.status = status
    info.email = email
    info.expires_at = expires_at
    info.error_message = error_message
    return info


def _create_mockcalendar_service(
    *,
    providers_connected: dict[str, bool] | None = None,
    provider_emails: dict[str, str] | None = None,
) -> MagicMock:
    """Create a mock calendar service with controllable state."""
    providers_connected = providers_connected or {}
    provider_emails = provider_emails or {}

    service = MagicMock()

    async def get_connection_status(provider: str) -> OAuthConnectionInfo:
        is_connected = providers_connected.get(provider, False)
        email = provider_emails.get(provider)
        return _create_mock_connection_info(
            provider=provider,
            status=IntegrationStatus.CONNECTED.value if is_connected else IntegrationStatus.DISCONNECTED.value,
            email=email,
            expires_at=datetime.now(UTC) + timedelta(hours=1) if is_connected else None,
        )

    service.get_connection_status = AsyncMock(side_effect=get_connection_status)
    service.initiate_oauth = AsyncMock()
    service.complete_oauth = AsyncMock()
    service.disconnect = AsyncMock()

    return service


class TestGetCalendarProviders:
    """Tests for GetCalendarProviders RPC."""

    @pytest.mark.asyncio
    async def test_returns_available_providers(self) -> None:
        """Returns list of available calendar providers."""
        service = _create_mockcalendar_service()
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.GetCalendarProviders(
            noteflow_pb2.GetCalendarProvidersRequest(),
            _DummyContext(),
        )

        assert len(response.providers) == 2, "should return google and outlook"
        provider_names = [p.name for p in response.providers]
        assert "google" in provider_names, "should include google"
        assert "outlook" in provider_names, "should include outlook"

    @pytest.mark.asyncio
    async def test_returns_authentication_status_for_each_provider(self) -> None:
        """Returns is_authenticated flag for each provider."""
        service = _create_mockcalendar_service(
            providers_connected={"google": True, "outlook": False}
        )
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.GetCalendarProviders(
            noteflow_pb2.GetCalendarProvidersRequest(),
            _DummyContext(),
        )

        google = next(p for p in response.providers if p.name == "google")
        outlook = next(p for p in response.providers if p.name == "outlook")

        assert google.is_authenticated is True, "google should be authenticated"
        assert outlook.is_authenticated is False, "outlook should not be authenticated"

    @pytest.mark.asyncio
    async def test_returns_display_names(self) -> None:
        """Returns human-readable display names for providers."""
        service = _create_mockcalendar_service()
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.GetCalendarProviders(
            noteflow_pb2.GetCalendarProvidersRequest(),
            _DummyContext(),
        )

        google = next(p for p in response.providers if p.name == "google")
        outlook = next(p for p in response.providers if p.name == "outlook")

        assert google.display_name == "Google Calendar", "google should have correct display name"
        assert outlook.display_name == "Microsoft Outlook", "outlook should have correct display name"

    @pytest.mark.asyncio
    async def test_aborts_whencalendar_service_not_configured(self) -> None:
        """Aborts with UNAVAILABLE when calendar service is not configured."""
        servicer = NoteFlowServicer()
        context = _DummyContext()

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

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


class TestInitiateOAuth:
    """Tests for InitiateOAuth RPC."""

    @pytest.mark.asyncio
    async def test_returns_auth_url_and_state(self) -> None:
        """Returns authorization URL and state token on success."""
        service = _create_mockcalendar_service()
        service.initiate_oauth.return_value = (
            "https://accounts.google.com/o/oauth2/v2/auth?client_id=...",
            "state-token-123",
        )
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.InitiateOAuth(
            noteflow_pb2.InitiateOAuthRequest(provider="google"),
            _DummyContext(),
        )

        assert "accounts.google.com" in response.auth_url, "should return google auth url"
        assert response.state == "state-token-123", "should return state token"

    @pytest.mark.asyncio
    async def test_passes_provider_to_service(self) -> None:
        """Passes provider name to calendar service."""
        service = _create_mockcalendar_service()
        service.initiate_oauth.return_value = ("https://auth.url", "state")
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        await servicer.InitiateOAuth(
            noteflow_pb2.InitiateOAuthRequest(provider="outlook"),
            _DummyContext(),
        )

        service.initiate_oauth.assert_awaited_once_with(
            provider="outlook",
            redirect_uri=None,
        )

    @pytest.mark.asyncio
    async def test_passes_custom_redirect_uri(self) -> None:
        """Passes custom redirect URI when provided."""
        service = _create_mockcalendar_service()
        service.initiate_oauth.return_value = ("https://auth.url", "state")
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        await servicer.InitiateOAuth(
            noteflow_pb2.InitiateOAuthRequest(
                provider="google",
                redirect_uri="noteflow://oauth/callback",
            ),
            _DummyContext(),
        )

        service.initiate_oauth.assert_awaited_once_with(
            provider="google",
            redirect_uri="noteflow://oauth/callback",
        )

    @pytest.mark.asyncio
    async def test_aborts_on_invalid_provider(self) -> None:
        """Aborts with INVALID_ARGUMENT for unknown provider."""
        service = _create_mockcalendar_service()
        service.initiate_oauth.side_effect = CalendarServiceError("Unknown provider: unknown")
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))
        context = _DummyContext()

        with pytest.raises(AssertionError, match="abort called"):
            await servicer.InitiateOAuth(
                noteflow_pb2.InitiateOAuthRequest(provider="unknown"),
                context,
            )

        assert context.aborted, "should abort on invalid provider"
        assert "Unknown provider" in context.abort_details, "should include provider in error"

    @pytest.mark.asyncio
    async def test_aborts_when_initiate_service_unavailable(self) -> None:
        """Aborts when calendar service is not configured."""
        servicer = NoteFlowServicer()
        context = _DummyContext()

        with pytest.raises(AssertionError, match="abort called"):
            await servicer.InitiateOAuth(
                noteflow_pb2.InitiateOAuthRequest(provider="google"),
                context,
            )

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


class TestCompleteOAuth:
    """Tests for CompleteOAuth RPC."""

    @pytest.mark.asyncio
    async def test_returns_success_on_valid_code(self) -> None:
        """Returns success=True when OAuth flow completes successfully."""
        service = _create_mockcalendar_service(
            providers_connected={"google": True},
            provider_emails={"google": "user@gmail.com"},
        )
        service.complete_oauth.return_value = True
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.CompleteOAuth(
            noteflow_pb2.CompleteOAuthRequest(
                provider="google",
                code="authorization-code",
                state="state-token-123",
            ),
            _DummyContext(),
        )

        assert response.success is True, "should succeed on valid code"
        assert response.provider_email == "user@gmail.com", "should return email"

    @pytest.mark.asyncio
    async def test_passes_code_and_state_to_service(self) -> None:
        """Passes authorization code and state to calendar service."""
        service = _create_mockcalendar_service()
        service.complete_oauth.return_value = True
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        await servicer.CompleteOAuth(
            noteflow_pb2.CompleteOAuthRequest(
                provider="google",
                code="my-auth-code",
                state="my-state-token",
            ),
            _DummyContext(),
        )

        service.complete_oauth.assert_awaited_once_with(
            provider="google",
            code="my-auth-code",
            state="my-state-token",
        )

    @pytest.mark.asyncio
    async def test_returns_error_on_invalid_state(self) -> None:
        """Returns success=False with error message for invalid state."""
        service = _create_mockcalendar_service()
        service.complete_oauth.side_effect = CalendarServiceError(
            "Invalid or expired state token"
        )
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.CompleteOAuth(
            noteflow_pb2.CompleteOAuthRequest(
                provider="google",
                code="authorization-code",
                state="invalid-state",
            ),
            _DummyContext(),
        )

        assert response.success is False, "should fail on invalid state"
        assert "Invalid or expired state" in response.error_message, "error should mention invalid state"

    @pytest.mark.asyncio
    async def test_returns_error_on_invalid_code(self) -> None:
        """Returns success=False with error message for invalid code."""
        service = _create_mockcalendar_service()
        service.complete_oauth.side_effect = CalendarServiceError(
            "Token exchange failed: invalid_grant"
        )
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.CompleteOAuth(
            noteflow_pb2.CompleteOAuthRequest(
                provider="google",
                code="invalid-code",
                state="valid-state",
            ),
            _DummyContext(),
        )

        assert response.success is False, "should fail on invalid code"
        assert "Token exchange failed" in response.error_message, "error should mention token exchange failure"

    @pytest.mark.asyncio
    async def test_aborts_when_complete_service_unavailable(self) -> None:
        """Aborts when calendar service is not configured."""
        servicer = NoteFlowServicer()
        context = _DummyContext()

        with pytest.raises(AssertionError, match="abort called"):
            await servicer.CompleteOAuth(
                noteflow_pb2.CompleteOAuthRequest(
                    provider="google",
                    code="code",
                    state="state",
                ),
                context,
            )

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


class TestGetOAuthConnectionStatus:
    """Tests for GetOAuthConnectionStatus RPC."""

    @pytest.mark.asyncio
    async def test_returns_connected_status(self) -> None:
        """Returns connection info when provider is connected."""
        service = _create_mockcalendar_service(
            providers_connected={"google": True},
            provider_emails={"google": "user@gmail.com"},
        )
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.GetOAuthConnectionStatus(
            noteflow_pb2.GetOAuthConnectionStatusRequest(provider="google"),
            _DummyContext(),
        )

        assert response.connection.provider == "google", "should return correct provider"
        assert response.connection.status == IntegrationStatus.CONNECTED.value, "status should be connected"
        assert response.connection.email == "user@gmail.com", "should return connected email"

    @pytest.mark.asyncio
    async def test_returns_disconnected_status(self) -> None:
        """Returns disconnected status when provider not connected."""
        service = _create_mockcalendar_service(
            providers_connected={"google": False}
        )
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.GetOAuthConnectionStatus(
            noteflow_pb2.GetOAuthConnectionStatusRequest(provider="google"),
            _DummyContext(),
        )

        assert response.connection.status == IntegrationStatus.DISCONNECTED.value, "status should be disconnected"

    @pytest.mark.asyncio
    async def test_returns_integration_type(self) -> None:
        """Returns correct integration type in response."""
        service = _create_mockcalendar_service()
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.GetOAuthConnectionStatus(
            noteflow_pb2.GetOAuthConnectionStatusRequest(
                provider="google",
                integration_type="calendar",
            ),
            _DummyContext(),
        )

        assert response.connection.integration_type == "calendar", "should return calendar integration type"

    @pytest.mark.asyncio
    async def test_aborts_when_status_service_unavailable(self) -> None:
        """Aborts when calendar service is not configured."""
        servicer = NoteFlowServicer()
        context = _DummyContext()

        with pytest.raises(AssertionError, match="abort called"):
            await servicer.GetOAuthConnectionStatus(
                noteflow_pb2.GetOAuthConnectionStatusRequest(provider="google"),
                context,
            )

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


class TestDisconnectOAuth:
    """Tests for DisconnectOAuth RPC."""

    @pytest.mark.asyncio
    async def test_returns_success_on_disconnect(self) -> None:
        """Returns success=True when disconnection succeeds."""
        service = _create_mockcalendar_service(
            providers_connected={"google": True}
        )
        service.disconnect.return_value = True
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.DisconnectOAuth(
            noteflow_pb2.DisconnectOAuthRequest(provider="google"),
            _DummyContext(),
        )

        assert response.success is True, "should succeed on disconnect"

    @pytest.mark.asyncio
    async def test_calls_service_disconnect(self) -> None:
        """Calls calendar service disconnect with correct provider."""
        service = _create_mockcalendar_service()
        service.disconnect.return_value = True
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        await servicer.DisconnectOAuth(
            noteflow_pb2.DisconnectOAuthRequest(provider="outlook"),
            _DummyContext(),
        )

        service.disconnect.assert_awaited_once_with("outlook")

    @pytest.mark.asyncio
    async def test_returns_false_when_not_connected(self) -> None:
        """Returns success=False when provider was not connected."""
        service = _create_mockcalendar_service(
            providers_connected={"google": False}
        )
        service.disconnect.return_value = False
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.DisconnectOAuth(
            noteflow_pb2.DisconnectOAuthRequest(provider="google"),
            _DummyContext(),
        )

        assert response.success is False, "should fail when not connected"

    @pytest.mark.asyncio
    async def test_aborts_when_disconnect_service_unavailable(self) -> None:
        """Aborts when calendar service is not configured."""
        servicer = NoteFlowServicer()
        context = _DummyContext()

        with pytest.raises(AssertionError, match="abort called"):
            await servicer.DisconnectOAuth(
                noteflow_pb2.DisconnectOAuthRequest(provider="google"),
                context,
            )

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


class TestOAuthRoundTrip:
    """Integration tests for complete OAuth flow."""

    @pytest.fixture
    def oauth_flow_service(self) -> tuple[MagicMock, dict[str, bool], dict[str, str]]:
        """Create service with mutable state for round-trip testing."""
        connected_state: dict[str, bool] = {"google": False}
        email_state: dict[str, str] = {}
        service = _create_mockcalendar_service()

        service.initiate_oauth.return_value = (
            "https://accounts.google.com/oauth",
            "state-123",
        )

        async def complete_oauth(provider: str, code: str, state: str) -> bool:
            if state != "state-123":
                raise CalendarServiceError("Invalid state")
            connected_state[provider] = True
            email_state[provider] = "user@gmail.com"
            return True

        service.complete_oauth.side_effect = complete_oauth

        async def get_status(provider: str) -> OAuthConnectionInfo:
            is_connected = connected_state.get(provider, False)
            return _create_mock_connection_info(
                provider=provider,
                status=IntegrationStatus.CONNECTED.value if is_connected else IntegrationStatus.DISCONNECTED.value,
                email=email_state.get(provider),
            )

        service.get_connection_status.side_effect = get_status

        async def disconnect(provider: str) -> bool:
            if connected_state.get(provider, False):
                connected_state[provider] = False
                email_state.pop(provider, None)
                return True
            return False

        service.disconnect.side_effect = disconnect

        return service, connected_state, email_state

    @pytest.mark.asyncio
    async def test_initiate_returns_auth_url(
        self, oauth_flow_service: tuple[MagicMock, dict[str, bool], dict[str, str]]
    ) -> None:
        """OAuth initiation returns auth URL and state."""
        service, _, _ = oauth_flow_service
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.InitiateOAuth(
            noteflow_pb2.InitiateOAuthRequest(provider="google"),
            _DummyContext(),
        )

        assert "google" in response.auth_url, "should contain google in url"
        assert response.state == "state-123", "should return state token"

    @pytest.mark.asyncio
    async def test_complete_updates_connection_status(
        self, oauth_flow_service: tuple[MagicMock, dict[str, bool], dict[str, str]]
    ) -> None:
        """Completing OAuth updates connection status."""
        service, connected_state, _ = oauth_flow_service
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        assert connected_state["google"] is False, "should start disconnected"

        response = await servicer.CompleteOAuth(
            noteflow_pb2.CompleteOAuthRequest(
                provider="google",
                code="auth-code",
                state="state-123",
            ),
            _DummyContext(),
        )

        assert response.success is True, "should complete successfully"
        assert connected_state["google"], "should be connected after complete"

    @pytest.mark.asyncio
    async def test_disconnect_clears_connection(
        self, oauth_flow_service: tuple[MagicMock, dict[str, bool], dict[str, str]]
    ) -> None:
        """Disconnecting clears connection status."""
        service, connected_state, email_state = oauth_flow_service
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        # First connect
        connected_state["google"] = True
        email_state["google"] = "user@gmail.com"

        response = await servicer.DisconnectOAuth(
            noteflow_pb2.DisconnectOAuthRequest(provider="google"),
            _DummyContext(),
        )

        assert response.success is True, "should disconnect successfully"
        assert not connected_state["google"], "should be disconnected"

    @pytest.mark.asyncio
    async def test_complete_with_wrong_state_fails(self) -> None:
        """Completing OAuth with wrong state token fails gracefully."""
        service = _create_mockcalendar_service()
        service.initiate_oauth.return_value = ("https://auth.url", "correct-state")
        service.complete_oauth.side_effect = CalendarServiceError(
            "Invalid or expired state token"
        )
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.CompleteOAuth(
            noteflow_pb2.CompleteOAuthRequest(
                provider="google",
                code="auth-code",
                state="wrong-state",
            ),
            _DummyContext(),
        )

        assert response.success is False, "should fail with wrong state"
        assert "Invalid or expired state" in response.error_message, "error should mention invalid state"

    @pytest.mark.asyncio
    async def test_multiple_providers_independent(self) -> None:
        """Multiple providers can be connected independently."""
        service = _create_mockcalendar_service(
            providers_connected={"google": True, "outlook": False},
            provider_emails={"google": "user@gmail.com"},
        )
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))
        ctx = _DummyContext()

        google_status = await servicer.GetOAuthConnectionStatus(
            noteflow_pb2.GetOAuthConnectionStatusRequest(provider="google"),
            ctx,
        )
        outlook_status = await servicer.GetOAuthConnectionStatus(
            noteflow_pb2.GetOAuthConnectionStatusRequest(provider="outlook"),
            ctx,
        )

        assert google_status.connection.status == IntegrationStatus.CONNECTED.value, "google should be connected"
        assert outlook_status.connection.status == IntegrationStatus.DISCONNECTED.value, "outlook should be disconnected"


class TestOAuthSecurityBehavior:
    """Tests for OAuth security requirements."""

    @pytest.mark.asyncio
    async def test_state_validation_required(self) -> None:
        """State token must match for completion to succeed."""
        service = _create_mockcalendar_service()
        service.initiate_oauth.return_value = ("https://auth.url", "secure-state-123")
        service.complete_oauth.side_effect = CalendarServiceError("State mismatch")
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.CompleteOAuth(
            noteflow_pb2.CompleteOAuthRequest(
                provider="google",
                code="stolen-code",
                state="attacker-state",
            ),
            _DummyContext(),
        )

        assert response.success is False, "should reject mismatched state"

    @pytest.mark.asyncio
    async def test_tokens_revoked_on_disconnect(self) -> None:
        """Disconnect should call service to revoke tokens."""
        service = _create_mockcalendar_service(
            providers_connected={"google": True}
        )
        service.disconnect.return_value = True
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        await servicer.DisconnectOAuth(
            noteflow_pb2.DisconnectOAuthRequest(provider="google"),
            _DummyContext(),
        )

        service.disconnect.assert_awaited_once_with("google")

    @pytest.mark.asyncio
    async def test_no_sensitive_data_in_error_responses(self) -> None:
        """Error responses should not leak sensitive information."""
        service = _create_mockcalendar_service()
        service.complete_oauth.side_effect = CalendarServiceError(
            "Token exchange failed: invalid_grant"
        )
        servicer = NoteFlowServicer(services=ServicesConfig(calendar_service=service))

        response = await servicer.CompleteOAuth(
            noteflow_pb2.CompleteOAuthRequest(
                provider="google",
                code="code",
                state="state",
            ),
            _DummyContext(),
        )

        assert "Bearer" not in response.error_message, "error should not leak bearer tokens"
        assert "secret" not in response.error_message.lower(), "error should not leak secrets"
