"""Tests for Discord OAuth service.

Tests the Discord OAuth flow with mocked HTTP responses using respx.
"""

from unittest.mock import patch

import pytest
import respx
from httpx import Response

from app.services.oauth.discord import DiscordOAuth, DiscordOAuthError

pytestmark = pytest.mark.asyncio


class TestGetAuthorizationUrl:
    """Tests for get_authorization_url method."""

    def test_raises_when_not_configured(self):
        """Test that get_authorization_url raises when Discord OAuth is not configured.

        Without client ID, the method should raise DiscordOAuthError.
        """
        oauth = DiscordOAuth()

        with patch("app.services.oauth.discord.settings") as mock_settings:
            mock_settings.discord_client_id = None

            with pytest.raises(DiscordOAuthError, match="not configured"):
                oauth.get_authorization_url("http://localhost/callback", "state123")

    def test_returns_valid_url_when_configured(self):
        """Test that get_authorization_url returns properly formatted URL.

        The URL should include client_id, redirect_uri, state, and scopes.
        """
        oauth = DiscordOAuth()

        with patch("app.services.oauth.discord.settings") as mock_settings:
            mock_settings.discord_client_id = "test-client-id"
            mock_settings.discord_client_secret = "test-secret"

            url = oauth.get_authorization_url("http://localhost/callback", "state123")

            assert "discord.com/api/oauth2/authorize" in url
            assert "client_id=test-client-id" in url
            assert "redirect_uri=http" in url
            assert "state=state123" in url
            assert "scope=" in url
            assert "response_type=code" in url


class TestExchangeCodeForTokens:
    """Tests for exchange_code_for_tokens method."""

    @respx.mock
    async def test_returns_tokens_on_success(self):
        """Test that exchange_code_for_tokens returns tokens on success.

        Mocks Discord's token endpoint to return valid tokens.
        """
        oauth = DiscordOAuth()

        respx.post("https://discord.com/api/oauth2/token").mock(
            return_value=Response(
                200,
                json={
                    "access_token": "test-access-token",
                    "refresh_token": "test-refresh-token",
                    "expires_in": 604800,
                    "token_type": "Bearer",
                },
            )
        )

        with patch("app.services.oauth.discord.settings") as mock_settings:
            mock_settings.discord_client_id = "test-client-id"
            mock_settings.discord_client_secret.get_secret_value.return_value = "test-secret"

            tokens = await oauth.exchange_code_for_tokens("auth-code", "http://localhost/callback")

            assert tokens["access_token"] == "test-access-token"

    @respx.mock
    async def test_raises_on_error_response(self):
        """Test that exchange_code_for_tokens raises on error from Discord.

        If Discord returns an error, DiscordOAuthError should be raised.
        """
        oauth = DiscordOAuth()

        respx.post("https://discord.com/api/oauth2/token").mock(
            return_value=Response(
                400,
                json={
                    "error": "invalid_grant",
                    "error_description": "Invalid code",
                },
            )
        )

        with patch("app.services.oauth.discord.settings") as mock_settings:
            mock_settings.discord_client_id = "test-client-id"
            mock_settings.discord_client_secret.get_secret_value.return_value = "test-secret"

            with pytest.raises(DiscordOAuthError, match="Token exchange failed"):
                await oauth.exchange_code_for_tokens("invalid-code", "http://localhost/callback")


class TestFetchUserInfo:
    """Tests for fetch_user_info method."""

    @respx.mock
    async def test_returns_user_info_on_success(self):
        """Test that fetch_user_info returns user data from Discord.

        Mocks Discord's users/@me endpoint.
        """
        oauth = DiscordOAuth()

        respx.get("https://discord.com/api/users/@me").mock(
            return_value=Response(
                200,
                json={
                    "id": "discord-user-123",
                    "username": "testuser",
                    "global_name": "Test User",
                    "email": "user@discord.com",
                    "avatar": "abc123",
                },
            )
        )

        user_info = await oauth.fetch_user_info("test-access-token")

        assert user_info["id"] == "discord-user-123"
        assert user_info["email"] == "user@discord.com"

    @respx.mock
    async def test_raises_on_error_response(self):
        """Test that fetch_user_info raises on error from Discord."""
        oauth = DiscordOAuth()

        respx.get("https://discord.com/api/users/@me").mock(
            return_value=Response(401, json={"message": "401: Unauthorized"})
        )

        with pytest.raises(DiscordOAuthError, match="Failed to fetch user info"):
            await oauth.fetch_user_info("invalid-token")


class TestBuildAvatarUrl:
    """Tests for _build_avatar_url method."""

    def test_returns_none_for_no_avatar(self):
        """Test that _build_avatar_url returns None when avatar is None."""
        oauth = DiscordOAuth()
        result = oauth._build_avatar_url("123456", None)
        assert result is None

    def test_builds_png_url_for_static_avatar(self):
        """Test that _build_avatar_url builds PNG URL for static avatars."""
        oauth = DiscordOAuth()
        result = oauth._build_avatar_url("123456", "abcdef123")

        assert result == "https://cdn.discordapp.com/avatars/123456/abcdef123.png"

    def test_builds_gif_url_for_animated_avatar(self):
        """Test that _build_avatar_url builds GIF URL for animated avatars.

        Animated avatars start with 'a_' prefix.
        """
        oauth = DiscordOAuth()
        result = oauth._build_avatar_url("123456", "a_animated123")

        assert result == "https://cdn.discordapp.com/avatars/123456/a_animated123.gif"


class TestGetUserInfo:
    """Tests for get_user_info method (full flow)."""

    @respx.mock
    async def test_returns_oauth_user_info_on_success(self):
        """Test that get_user_info completes full OAuth flow.

        This tests the combined token exchange + user info fetch.
        """
        oauth = DiscordOAuth()

        # Mock token exchange
        respx.post("https://discord.com/api/oauth2/token").mock(
            return_value=Response(
                200,
                json={
                    "access_token": "test-access-token",
                    "refresh_token": "test-refresh",
                    "expires_in": 604800,
                },
            )
        )

        # Mock user info
        respx.get("https://discord.com/api/users/@me").mock(
            return_value=Response(
                200,
                json={
                    "id": "discord-user-456",
                    "username": "fullflowuser",
                    "global_name": "Full Flow User",
                    "email": "fullflow@discord.com",
                    "avatar": "avatar123",
                },
            )
        )

        with patch("app.services.oauth.discord.settings") as mock_settings:
            mock_settings.discord_client_id = "test-client-id"
            mock_settings.discord_client_secret.get_secret_value.return_value = "test-secret"

            result = await oauth.get_user_info("auth-code", "http://localhost/callback")

            assert result.provider == "discord"
            assert result.oauth_id == "discord-user-456"
            assert result.email == "fullflow@discord.com"
            assert result.name == "Full Flow User"
            assert "avatar123.png" in result.avatar_url

    @respx.mock
    async def test_uses_username_when_no_global_name(self):
        """Test that get_user_info falls back to username for display name.

        Discord users may not have global_name set.
        """
        oauth = DiscordOAuth()

        respx.post("https://discord.com/api/oauth2/token").mock(
            return_value=Response(
                200,
                json={"access_token": "test-access-token"},
            )
        )

        respx.get("https://discord.com/api/users/@me").mock(
            return_value=Response(
                200,
                json={
                    "id": "discord-user-789",
                    "username": "legacyuser",
                    "global_name": None,  # No global name
                    "email": "legacy@discord.com",
                    "avatar": None,
                },
            )
        )

        with patch("app.services.oauth.discord.settings") as mock_settings:
            mock_settings.discord_client_id = "test-client-id"
            mock_settings.discord_client_secret.get_secret_value.return_value = "test-secret"

            result = await oauth.get_user_info("auth-code", "http://localhost/callback")

            assert result.name == "legacyuser"
            assert result.avatar_url is None

    @respx.mock
    async def test_raises_when_no_email(self):
        """Test that get_user_info raises when Discord user has no email.

        Email is required for account creation.
        """
        oauth = DiscordOAuth()

        respx.post("https://discord.com/api/oauth2/token").mock(
            return_value=Response(
                200,
                json={"access_token": "test-access-token"},
            )
        )

        respx.get("https://discord.com/api/users/@me").mock(
            return_value=Response(
                200,
                json={
                    "id": "discord-user-noemail",
                    "username": "noemailuser",
                    "email": None,  # No verified email
                },
            )
        )

        with patch("app.services.oauth.discord.settings") as mock_settings:
            mock_settings.discord_client_id = "test-client-id"
            mock_settings.discord_client_secret.get_secret_value.return_value = "test-secret"

            with pytest.raises(DiscordOAuthError, match="verified email"):
                await oauth.get_user_info("auth-code", "http://localhost/callback")


class TestIsConfigured:
    """Tests for is_configured method."""

    def test_returns_false_when_not_configured(self):
        """Test that is_configured returns False without credentials."""
        oauth = DiscordOAuth()

        with patch("app.services.oauth.discord.settings") as mock_settings:
            mock_settings.discord_client_id = None
            mock_settings.discord_client_secret = None

            assert oauth.is_configured() is False

    def test_returns_true_when_configured(self):
        """Test that is_configured returns True with credentials."""
        oauth = DiscordOAuth()

        with patch("app.services.oauth.discord.settings") as mock_settings:
            mock_settings.discord_client_id = "client-id"
            mock_settings.discord_client_secret = "client-secret"

            assert oauth.is_configured() is True
