"""Tests for UserService.

Tests the user service CRUD operations and OAuth-based user creation.
Uses real Postgres via the db_session fixture from conftest.

The UserService now uses dependency injection with repositories injected
via constructor, so we create a fresh service instance per test.
"""

from datetime import UTC, datetime, timedelta
from uuid import UUID

import pytest

from app.db.models import User
from app.db.models.oauth_account import OAuthLinkedAccount
from app.repositories.postgres.linked_account import PostgresLinkedAccountRepository
from app.repositories.postgres.user import PostgresUserRepository
from app.schemas.user import OAuthUserInfo, UserUpdate
from app.services.user_service import AccountLinkingError, UserService

# Import db_session fixture from db conftest
pytestmark = pytest.mark.asyncio


@pytest.fixture
def user_service(db_session):
    """Create UserService with real PostgreSQL repositories.

    This fixture provides a properly constructed UserService for each test,
    following the dependency injection pattern used in production.
    """
    user_repo = PostgresUserRepository(db_session)
    linked_repo = PostgresLinkedAccountRepository(db_session)
    return UserService(user_repo, linked_repo)


class TestGetById:
    """Tests for get_by_id method."""

    async def test_returns_user_when_found(self, db_session, user_service):
        """Test that get_by_id returns user when it exists.

        Creates a user and verifies it can be retrieved by ID.
        """
        # Create user directly
        user = User(
            email="test@example.com",
            display_name="Test User",
            oauth_provider="google",
            oauth_id="123456",
        )
        db_session.add(user)
        await db_session.commit()

        # Retrieve by ID
        user_id = UUID(user.id) if isinstance(user.id, str) else user.id
        result = await user_service.get_by_id(user_id)

        assert result is not None
        assert result.email == "test@example.com"

    async def test_returns_none_when_not_found(self, user_service):
        """Test that get_by_id returns None for nonexistent users."""
        from uuid import uuid4

        result = await user_service.get_by_id(uuid4())
        assert result is None


class TestGetByEmail:
    """Tests for get_by_email method."""

    async def test_returns_user_when_found(self, db_session, user_service):
        """Test that get_by_email returns user when it exists."""
        user = User(
            email="findme@example.com",
            display_name="Find Me",
            oauth_provider="discord",
            oauth_id="discord123",
        )
        db_session.add(user)
        await db_session.commit()

        result = await user_service.get_by_email("findme@example.com")

        assert result is not None
        assert result.display_name == "Find Me"

    async def test_returns_none_when_not_found(self, user_service):
        """Test that get_by_email returns None for nonexistent emails."""
        result = await user_service.get_by_email("nobody@example.com")
        assert result is None


class TestGetByOAuth:
    """Tests for get_by_oauth method."""

    async def test_returns_user_when_found(self, db_session, user_service):
        """Test that get_by_oauth returns user for matching provider+id."""
        user = User(
            email="oauth@example.com",
            display_name="OAuth User",
            oauth_provider="google",
            oauth_id="google-unique-id",
        )
        db_session.add(user)
        await db_session.commit()

        result = await user_service.get_by_oauth("google", "google-unique-id")

        assert result is not None
        assert result.email == "oauth@example.com"

    async def test_returns_none_for_wrong_provider(self, db_session, user_service):
        """Test that get_by_oauth returns None if provider doesn't match."""
        user = User(
            email="oauth2@example.com",
            display_name="OAuth User 2",
            oauth_provider="google",
            oauth_id="google-id-2",
        )
        db_session.add(user)
        await db_session.commit()

        # Same ID, different provider
        result = await user_service.get_by_oauth("discord", "google-id-2")
        assert result is None

    async def test_returns_none_when_not_found(self, user_service):
        """Test that get_by_oauth returns None for nonexistent OAuth."""
        result = await user_service.get_by_oauth("google", "nonexistent")
        assert result is None


class TestCreate:
    """Tests for create method."""

    async def test_creates_user_with_all_fields(self, user_service):
        """Test that create properly persists all user fields."""
        result = await user_service.create(
            email="new@example.com",
            display_name="New User",
            oauth_provider="discord",
            oauth_id="discord-new-id",
            avatar_url="https://example.com/avatar.jpg",
        )

        assert result.id is not None
        assert result.email == "new@example.com"
        assert result.display_name == "New User"
        assert result.avatar_url == "https://example.com/avatar.jpg"
        assert result.oauth_provider == "discord"
        assert result.oauth_id == "discord-new-id"
        assert result.is_premium is False
        assert result.premium_until is None

    async def test_creates_user_without_avatar(self, user_service):
        """Test that create works without optional avatar_url."""
        result = await user_service.create(
            email="noavatar@example.com",
            display_name="No Avatar",
            oauth_provider="google",
            oauth_id="google-no-avatar",
        )

        assert result.avatar_url is None


class TestCreateFromOAuth:
    """Tests for create_from_oauth method."""

    async def test_creates_user_from_oauth_info(self, user_service):
        """Test that create_from_oauth converts OAuthUserInfo to User."""
        oauth_info = OAuthUserInfo(
            provider="google",
            oauth_id="google-oauth-123",
            email="oauthcreate@example.com",
            name="OAuth Created User",
            avatar_url="https://google.com/avatar.jpg",
        )

        result = await user_service.create_from_oauth(oauth_info)

        assert result.email == "oauthcreate@example.com"
        assert result.display_name == "OAuth Created User"
        assert result.oauth_provider == "google"
        assert result.oauth_id == "google-oauth-123"


class TestGetOrCreateFromOAuth:
    """Tests for get_or_create_from_oauth method."""

    async def test_returns_existing_user_by_oauth(self, db_session, user_service):
        """Test that existing user is returned when OAuth matches.

        Verifies the method returns (user, False) for existing users.
        """
        # Create existing user
        existing = User(
            email="existing@example.com",
            display_name="Existing",
            oauth_provider="google",
            oauth_id="existing-oauth-id",
        )
        db_session.add(existing)
        await db_session.commit()

        # Try to get or create with same OAuth
        oauth_info = OAuthUserInfo(
            provider="google",
            oauth_id="existing-oauth-id",
            email="existing@example.com",
            name="Existing",
        )

        result, created = await user_service.get_or_create_from_oauth(oauth_info)

        assert created is False
        assert str(result.id) == str(existing.id)

    async def test_links_existing_user_by_email(self, db_session, user_service):
        """Test that OAuth is linked when email matches existing user.

        If a user exists with the same email but different OAuth,
        the new OAuth should be linked to the existing account.
        """
        # Create user with Google
        existing = User(
            email="link@example.com",
            display_name="Link Me",
            oauth_provider="google",
            oauth_id="google-link-id",
        )
        db_session.add(existing)
        await db_session.commit()

        # Login with Discord (same email)
        oauth_info = OAuthUserInfo(
            provider="discord",
            oauth_id="discord-link-id",
            email="link@example.com",
            name="Link Me",
            avatar_url="https://discord.com/avatar.jpg",
        )

        result, created = await user_service.get_or_create_from_oauth(oauth_info)

        assert created is False
        assert str(result.id) == str(existing.id)
        # OAuth should be updated to Discord
        assert result.oauth_provider == "discord"
        assert result.oauth_id == "discord-link-id"

    async def test_creates_new_user_when_not_found(self, user_service):
        """Test that new user is created when no match exists.

        Verifies the method returns (user, True) for new users.
        """
        oauth_info = OAuthUserInfo(
            provider="discord",
            oauth_id="brand-new-id",
            email="brandnew@example.com",
            name="Brand New",
        )

        result, created = await user_service.get_or_create_from_oauth(oauth_info)

        assert created is True
        assert result.email == "brandnew@example.com"


class TestUpdate:
    """Tests for update method."""

    async def test_updates_display_name(self, db_session, user_service):
        """Test that update changes display_name when provided."""
        user = User(
            email="update@example.com",
            display_name="Old Name",
            oauth_provider="google",
            oauth_id="update-id",
        )
        db_session.add(user)
        await db_session.commit()

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id
        update_data = UserUpdate(display_name="New Name")
        result = await user_service.update(user_id, update_data)

        assert result.display_name == "New Name"

    async def test_updates_avatar_url(self, db_session, user_service):
        """Test that update changes avatar_url when provided."""
        user = User(
            email="avatar@example.com",
            display_name="Avatar User",
            oauth_provider="google",
            oauth_id="avatar-id",
        )
        db_session.add(user)
        await db_session.commit()

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id
        update_data = UserUpdate(avatar_url="https://new-avatar.com/img.jpg")
        result = await user_service.update(user_id, update_data)

        assert result.avatar_url == "https://new-avatar.com/img.jpg"

    async def test_ignores_none_values(self, db_session, user_service):
        """Test that update doesn't change fields set to None.

        Only explicitly provided fields should be updated.
        """
        user = User(
            email="keep@example.com",
            display_name="Keep Me",
            avatar_url="https://keep.com/avatar.jpg",
            oauth_provider="google",
            oauth_id="keep-id",
        )
        db_session.add(user)
        await db_session.commit()

        # Update only display_name, leave avatar alone
        user_id = UUID(user.id) if isinstance(user.id, str) else user.id
        update_data = UserUpdate(display_name="Changed")
        result = await user_service.update(user_id, update_data)

        assert result.display_name == "Changed"
        assert result.avatar_url == "https://keep.com/avatar.jpg"


class TestUpdateLastLogin:
    """Tests for update_last_login method."""

    async def test_updates_last_login_timestamp(self, db_session, user_service):
        """Test that update_last_login sets current timestamp."""
        user = User(
            email="login@example.com",
            display_name="Login User",
            oauth_provider="google",
            oauth_id="login-id",
        )
        db_session.add(user)
        await db_session.commit()

        assert user.last_login is None

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id
        before = datetime.now(UTC)
        result = await user_service.update_last_login(user_id)
        after = datetime.now(UTC)

        assert result.last_login is not None
        # Allow 1 second tolerance
        assert before - timedelta(seconds=1) <= result.last_login <= after + timedelta(seconds=1)


class TestUpdatePremium:
    """Tests for update_premium method."""

    async def test_grants_premium(self, db_session, user_service):
        """Test that update_premium sets premium status and expiration."""
        user = User(
            email="premium@example.com",
            display_name="Premium User",
            oauth_provider="google",
            oauth_id="premium-id",
        )
        db_session.add(user)
        await db_session.commit()

        assert user.is_premium is False

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id
        expires = datetime.now(UTC) + timedelta(days=30)
        result = await user_service.update_premium(user_id, expires)

        assert result.is_premium is True
        assert result.premium_until == expires

    async def test_removes_premium(self, db_session, user_service):
        """Test that update_premium with None removes premium status."""
        user = User(
            email="unpremium@example.com",
            display_name="Unpremium User",
            oauth_provider="google",
            oauth_id="unpremium-id",
            is_premium=True,
            premium_until=datetime.now(UTC) + timedelta(days=30),
        )
        db_session.add(user)
        await db_session.commit()

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id
        result = await user_service.update_premium(user_id, None)

        assert result.is_premium is False
        assert result.premium_until is None


class TestDelete:
    """Tests for delete method."""

    async def test_deletes_user(self, db_session, user_service):
        """Test that delete removes user from database."""
        user = User(
            email="delete@example.com",
            display_name="Delete Me",
            oauth_provider="google",
            oauth_id="delete-id",
        )
        db_session.add(user)
        await db_session.commit()

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id
        await user_service.delete(user_id)

        # Verify user is gone
        result = await user_service.get_by_id(user_id)
        assert result is None


class TestGetLinkedAccount:
    """Tests for get_linked_account method."""

    async def test_returns_linked_account_when_found(self, db_session, user_service):
        """Test that get_linked_account returns account when it exists.

        Creates a user with a linked account and verifies it can be retrieved.
        """
        # Create user
        user = User(
            email="primary@example.com",
            display_name="Primary User",
            oauth_provider="google",
            oauth_id="google-primary",
        )
        db_session.add(user)
        await db_session.commit()

        # Create linked account
        linked = OAuthLinkedAccount(
            user_id=user.id,
            provider="discord",
            oauth_id="discord-linked-123",
            email="linked@example.com",
        )
        db_session.add(linked)
        await db_session.commit()

        # Retrieve linked account
        result = await user_service.get_linked_account("discord", "discord-linked-123")

        assert result is not None
        assert result.provider == "discord"
        assert result.oauth_id == "discord-linked-123"

    async def test_returns_none_when_not_found(self, user_service):
        """Test that get_linked_account returns None for nonexistent accounts."""
        result = await user_service.get_linked_account("discord", "nonexistent-id")
        assert result is None


class TestLinkOAuthAccount:
    """Tests for link_oauth_account method."""

    async def test_links_new_provider(self, db_session, user_service):
        """Test that link_oauth_account successfully links a new provider.

        Creates a Google user and links Discord to them.
        """
        # Create user with Google
        user = User(
            email="google-user@example.com",
            display_name="Google User",
            oauth_provider="google",
            oauth_id="google-123",
        )
        db_session.add(user)
        await db_session.commit()
        await db_session.refresh(user)

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id

        # Link Discord
        discord_info = OAuthUserInfo(
            provider="discord",
            oauth_id="discord-456",
            email="discord@example.com",
            name="Discord Name",
            avatar_url="https://discord.com/avatar.png",
        )

        result = await user_service.link_oauth_account(user_id, user.oauth_provider, discord_info)

        assert result is not None
        assert result.provider == "discord"
        assert result.oauth_id == "discord-456"
        assert result.email == "discord@example.com"
        assert result.display_name == "Discord Name"
        assert str(result.user_id) == str(user_id)

    async def test_raises_error_if_already_linked_to_same_user(self, db_session, user_service):
        """Test that linking same provider twice raises error.

        A user cannot have the same provider linked multiple times.
        """
        user = User(
            email="double-link@example.com",
            display_name="Double Link",
            oauth_provider="google",
            oauth_id="google-double",
        )
        db_session.add(user)
        await db_session.commit()
        await db_session.refresh(user)

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id

        # Link Discord first time
        discord_info = OAuthUserInfo(
            provider="discord",
            oauth_id="discord-first",
            email="first@discord.com",
            name="First",
        )
        await user_service.link_oauth_account(user_id, user.oauth_provider, discord_info)

        # Try to link same Discord account again
        with pytest.raises(AccountLinkingError) as exc_info:
            await user_service.link_oauth_account(user_id, user.oauth_provider, discord_info)

        assert "already linked to your account" in str(exc_info.value)

    async def test_raises_error_if_linked_to_another_user(self, db_session, user_service):
        """Test that linking account already linked to another user raises error.

        The same OAuth provider+ID cannot be linked to multiple users.
        """
        # Create first user and link Discord
        user1 = User(
            email="user1@example.com",
            display_name="User 1",
            oauth_provider="google",
            oauth_id="google-user1",
        )
        db_session.add(user1)
        await db_session.commit()
        await db_session.refresh(user1)

        user1_id = UUID(user1.id) if isinstance(user1.id, str) else user1.id

        discord_info = OAuthUserInfo(
            provider="discord",
            oauth_id="shared-discord",
            email="shared@discord.com",
            name="Shared",
        )
        await user_service.link_oauth_account(user1_id, user1.oauth_provider, discord_info)

        # Create second user
        user2 = User(
            email="user2@example.com",
            display_name="User 2",
            oauth_provider="google",
            oauth_id="google-user2",
        )
        db_session.add(user2)
        await db_session.commit()
        await db_session.refresh(user2)

        user2_id = UUID(user2.id) if isinstance(user2.id, str) else user2.id

        # Try to link same Discord account to second user
        with pytest.raises(AccountLinkingError) as exc_info:
            await user_service.link_oauth_account(user2_id, user2.oauth_provider, discord_info)

        assert "already linked to another user" in str(exc_info.value)

    async def test_raises_error_if_linking_primary_provider(self, db_session, user_service):
        """Test that linking the same provider as primary raises error.

        User cannot link Google if they already signed up with Google.
        """
        user = User(
            email="google-primary@example.com",
            display_name="Google Primary",
            oauth_provider="google",
            oauth_id="google-primary-id",
        )
        db_session.add(user)
        await db_session.commit()
        await db_session.refresh(user)

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id

        # Try to link another Google account
        google_info = OAuthUserInfo(
            provider="google",
            oauth_id="google-different-id",
            email="different@gmail.com",
            name="Different",
        )

        with pytest.raises(AccountLinkingError) as exc_info:
            await user_service.link_oauth_account(user_id, user.oauth_provider, google_info)

        assert "primary login provider" in str(exc_info.value)


class TestUnlinkOAuthAccount:
    """Tests for unlink_oauth_account method."""

    async def test_unlinks_linked_account(self, db_session, user_service):
        """Test that unlink_oauth_account removes a linked account.

        Links Discord then unlinks it successfully.
        """
        user = User(
            email="unlink@example.com",
            display_name="Unlink User",
            oauth_provider="google",
            oauth_id="google-unlink",
        )
        db_session.add(user)
        await db_session.commit()
        await db_session.refresh(user)

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id

        # Link Discord
        discord_info = OAuthUserInfo(
            provider="discord",
            oauth_id="discord-unlink",
            email="discord@unlink.com",
            name="Discord Unlink",
        )
        await user_service.link_oauth_account(user_id, user.oauth_provider, discord_info)

        # Unlink
        result = await user_service.unlink_oauth_account(user_id, user.oauth_provider, "discord")

        assert result is True

        # Verify unlinked
        linked = await user_service.get_linked_account("discord", "discord-unlink")
        assert linked is None

    async def test_returns_false_if_not_linked(self, db_session, user_service):
        """Test that unlink returns False if provider isn't linked."""
        user = User(
            email="not-linked@example.com",
            display_name="Not Linked",
            oauth_provider="google",
            oauth_id="google-notlinked",
        )
        db_session.add(user)
        await db_session.commit()
        await db_session.refresh(user)

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id
        result = await user_service.unlink_oauth_account(user_id, user.oauth_provider, "discord")

        assert result is False

    async def test_raises_error_if_unlinking_primary(self, db_session, user_service):
        """Test that unlinking primary provider raises error.

        User cannot unlink their primary OAuth provider.
        """
        user = User(
            email="primary-unlink@example.com",
            display_name="Primary Unlink",
            oauth_provider="google",
            oauth_id="google-primary-unlink",
        )
        db_session.add(user)
        await db_session.commit()
        await db_session.refresh(user)

        user_id = UUID(user.id) if isinstance(user.id, str) else user.id

        with pytest.raises(AccountLinkingError) as exc_info:
            await user_service.unlink_oauth_account(user_id, user.oauth_provider, "google")

        assert "primary login provider" in str(exc_info.value)
