"""Tests for OAuth manager."""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from noteflow.config.settings import CalendarIntegrationSettings
from noteflow.domain.value_objects import OAuthProvider
from noteflow.infrastructure.calendar.oauth_manager import OAuthError


class TestOAuthManagerInitiateAuth:
    """Tests for OAuthManager.initiate_auth."""

    def test_initiate_google_auth_returns_url_and_state(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """initiate_auth should return auth URL and state for Google."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)
        auth_url, state = manager.initiate_auth(OAuthProvider.GOOGLE, "http://localhost:8080/callback")

        assert auth_url.startswith("https://accounts.google.com/o/oauth2/v2/auth"), "should use Google auth URL"
        assert "client_id=test-google-client-id" in auth_url, "should include client_id"
        assert "redirect_uri=http" in auth_url, "should include redirect_uri"
        assert "code_challenge=" in auth_url, "should include PKCE code_challenge"
        assert "code_challenge_method=S256" in auth_url, "should use S256 code challenge"
        assert len(state) > 0, "should return non-empty state"

    def test_initiate_outlook_auth_returns_url_and_state(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """initiate_auth should return auth URL and state for Outlook."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)
        auth_url, state = manager.initiate_auth(OAuthProvider.OUTLOOK, "http://localhost:8080/callback")

        assert auth_url.startswith("https://login.microsoftonline.com/common/oauth2/v2.0/authorize"), "should use Outlook auth URL"
        assert "client_id=test-outlook-client-id" in auth_url, "should include Outlook client_id"
        assert len(state) > 0, "should return non-empty state"

    def test_initiate_auth_stores_pending_state(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """initiate_auth should store pending state for later validation."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)
        _, state = manager.initiate_auth(OAuthProvider.GOOGLE, "http://localhost:8080/callback")

        assert manager.has_pending_state(state), "state should be stored in pending_states"
        pending_state = manager.get_pending_state(state)
        assert pending_state is not None, "pending state should exist"
        assert pending_state.provider == OAuthProvider.GOOGLE, "should store correct provider"

    def test_initiate_auth_missing_credentials_raises(self) -> None:
        """initiate_auth should raise for missing OAuth credentials."""
        from noteflow.infrastructure.calendar import OAuthManager

        # Create settings with missing Google credentials
        settings = CalendarIntegrationSettings(
            google_client_id="",
            google_client_secret="",
            outlook_client_id="test-id",
            outlook_client_secret="test-secret",
            redirect_uri="http://localhost:8080/callback",
            sync_hours_ahead=24,
            max_events=20,
            sync_interval_minutes=15,
        )
        manager = OAuthManager(settings)

        with pytest.raises(OAuthError, match="not configured"):
            manager.initiate_auth(OAuthProvider.GOOGLE, "http://localhost:8080/callback")


class TestOAuthManagerCompleteAuth:
    """Tests for OAuthManager.complete_auth."""

    @pytest.mark.asyncio
    async def test_complete_auth_exchanges_code_for_tokens(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """complete_auth should exchange authorization code for tokens."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)
        _, state = manager.initiate_auth(OAuthProvider.GOOGLE, "http://localhost:8080/callback")

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "access-token-123",
            "refresh_token": "refresh-token-456",
            "token_type": "Bearer",
            "expires_in": 3600,
            "scope": "email calendar.readonly",
        }

        with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
            mock_post.return_value = mock_response
            tokens = await manager.complete_auth(OAuthProvider.GOOGLE, "auth-code-xyz", state)

        assert tokens.access_token == "access-token-123", "access_token should match"
        assert tokens.refresh_token == "refresh-token-456", "refresh_token should match"
        assert tokens.token_type == "Bearer", "token_type should be Bearer"

    @pytest.mark.asyncio
    async def test_complete_auth_invalid_state_raises(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """complete_auth should raise for invalid state."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)

        with pytest.raises(OAuthError, match="Invalid or expired"):
            await manager.complete_auth(OAuthProvider.GOOGLE, "code", "invalid-state")

    @pytest.mark.asyncio
    async def test_complete_auth_expired_state_raises(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """complete_auth should raise for expired state."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)
        _, state = manager.initiate_auth(OAuthProvider.GOOGLE, "http://localhost:8080/callback")

        # Expire the state manually by replacing the OAuthState
        old_state = manager.get_pending_state(state)
        assert old_state is not None, "state should exist"
        from noteflow.domain.value_objects import OAuthState
        expired_state = OAuthState(
            state=old_state.state,
            provider=old_state.provider,
            redirect_uri=old_state.redirect_uri,
            code_verifier=old_state.code_verifier,
            created_at=old_state.created_at,
            expires_at=datetime.now(UTC) - timedelta(minutes=1),
        )
        manager.set_pending_state(state, expired_state)

        with pytest.raises(OAuthError, match="expired"):
            await manager.complete_auth(OAuthProvider.GOOGLE, "code", state)

    @pytest.mark.asyncio
    async def test_complete_auth_removes_used_state(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """complete_auth should remove state after successful use."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)
        _, state = manager.initiate_auth(OAuthProvider.GOOGLE, "http://localhost:8080/callback")

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "token",
            "token_type": "Bearer",
            "expires_in": 3600,
        }

        with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
            mock_post.return_value = mock_response
            await manager.complete_auth(OAuthProvider.GOOGLE, "code", state)

        assert not manager.has_pending_state(state), "state should be removed after use"


class TestOAuthManagerRefreshTokens:
    """Tests for OAuthManager.refresh_tokens."""

    @pytest.mark.asyncio
    async def test_refresh_tokens_returns_new_tokens(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """refresh_tokens should return new access token."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "new-access-token",
            "token_type": "Bearer",
            "expires_in": 3600,
        }

        with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
            mock_post.return_value = mock_response
            tokens = await manager.refresh_tokens(OAuthProvider.GOOGLE, "old-refresh-token")

        assert tokens.access_token == "new-access-token", "should return new access token"

    @pytest.mark.asyncio
    async def test_refresh_tokens_preserves_refresh_token_if_not_returned(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """refresh_tokens should preserve old refresh token if not in response."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "new-access-token",
            "token_type": "Bearer",
            "expires_in": 3600,
        }

        with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
            mock_post.return_value = mock_response
            tokens = await manager.refresh_tokens(OAuthProvider.GOOGLE, "old-refresh-token")

        assert tokens.refresh_token == "old-refresh-token", "should preserve old refresh token"


class TestOAuthManagerRevokeTokens:
    """Tests for OAuthManager.revoke_tokens."""

    @pytest.mark.asyncio
    async def test_revoke_google_tokens_calls_revocation_endpoint(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """revoke_tokens should call Google revocation endpoint."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)

        mock_response = MagicMock()
        mock_response.status_code = 200

        with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
            mock_post.return_value = mock_response
            await manager.revoke_tokens(OAuthProvider.GOOGLE, "access-token")

        mock_post.assert_called_once()
        call_args = mock_post.call_args
        assert "oauth2.googleapis.com/revoke" in call_args[0][0], "should call Google revocation endpoint"

    @pytest.mark.asyncio
    async def test_revoke_outlook_tokens_handles_no_revocation_endpoint(
        self, calendar_settings: CalendarIntegrationSettings
    ) -> None:
        """revoke_tokens should succeed silently for Outlook (no revocation endpoint)."""
        from noteflow.infrastructure.calendar import OAuthManager

        manager = OAuthManager(calendar_settings)

        # Should not raise - Outlook doesn't have a revocation endpoint
        await manager.revoke_tokens(OAuthProvider.OUTLOOK, "access-token")
