# tests/services/test_auth_service.py
import uuid
from unittest.mock import patch

import pytest
from sqlalchemy import select

from app.core.auth import (
    TokenInvalidError,
    get_password_hash,
    verify_password,
)
from app.models.user import User
from app.schemas.users import Token, UserCreate
from app.services.auth_service import AuthenticationError, AuthService


class TestAuthServiceAuthentication:
    """Tests for AuthService.authenticate_user method"""

    @pytest.mark.asyncio
    async def test_authenticate_valid_user(self, async_test_db, async_test_user):
        """Test authenticating a user with valid credentials"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Set a known password for the mock user
        password = "TestPassword123!"
        async with AsyncTestingSessionLocal() as session:
            result = await session.execute(
                select(User).where(User.id == async_test_user.id)
            )
            user = result.scalar_one_or_none()
            user.password_hash = get_password_hash(password)
            await session.commit()

        # Authenticate with correct credentials
        async with AsyncTestingSessionLocal() as session:
            auth_user = await AuthService.authenticate_user(
                db=session, email=async_test_user.email, password=password
            )

            assert auth_user is not None
            assert auth_user.id == async_test_user.id
            assert auth_user.email == async_test_user.email

    @pytest.mark.asyncio
    async def test_authenticate_nonexistent_user(self, async_test_db):
        """Test authenticating with an email that doesn't exist"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            user = await AuthService.authenticate_user(
                db=session, email="nonexistent@example.com", password="password"
            )

            assert user is None

    @pytest.mark.asyncio
    async def test_authenticate_with_wrong_password(
        self, async_test_db, async_test_user
    ):
        """Test authenticating with the wrong password"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Set a known password for the mock user
        password = "TestPassword123!"
        async with AsyncTestingSessionLocal() as session:
            result = await session.execute(
                select(User).where(User.id == async_test_user.id)
            )
            user = result.scalar_one_or_none()
            user.password_hash = get_password_hash(password)
            await session.commit()

        # Authenticate with wrong password
        async with AsyncTestingSessionLocal() as session:
            auth_user = await AuthService.authenticate_user(
                db=session, email=async_test_user.email, password="WrongPassword123"
            )

            assert auth_user is None

    @pytest.mark.asyncio
    async def test_authenticate_inactive_user(self, async_test_db, async_test_user):
        """Test authenticating an inactive user"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Set a known password and make user inactive
        password = "TestPassword123!"
        async with AsyncTestingSessionLocal() as session:
            result = await session.execute(
                select(User).where(User.id == async_test_user.id)
            )
            user = result.scalar_one_or_none()
            user.password_hash = get_password_hash(password)
            user.is_active = False
            await session.commit()

        # Should raise AuthenticationError
        async with AsyncTestingSessionLocal() as session:
            with pytest.raises(AuthenticationError):
                await AuthService.authenticate_user(
                    db=session, email=async_test_user.email, password=password
                )


class TestAuthServiceUserCreation:
    """Tests for AuthService.create_user method"""

    @pytest.mark.asyncio
    async def test_create_new_user(self, async_test_db):
        """Test creating a new user"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        user_data = UserCreate(
            email="newuser@example.com",
            password="TestPassword123!",
            first_name="New",
            last_name="User",
            phone_number="+1234567890",
        )

        async with AsyncTestingSessionLocal() as session:
            user = await AuthService.create_user(db=session, user_data=user_data)

            # Verify user was created with correct data
            assert user is not None
            assert user.email == user_data.email
            assert user.first_name == user_data.first_name
            assert user.last_name == user_data.last_name
            assert user.phone_number == user_data.phone_number

            # Verify password was hashed
            assert user.password_hash != user_data.password
            assert verify_password(user_data.password, user.password_hash)

            # Verify default values
            assert user.is_active is True
            assert user.is_superuser is False

    @pytest.mark.asyncio
    async def test_create_user_with_existing_email(
        self, async_test_db, async_test_user
    ):
        """Test creating a user with an email that already exists"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        user_data = UserCreate(
            email=async_test_user.email,  # Use existing email
            password="TestPassword123!",
            first_name="Duplicate",
            last_name="User",
        )

        # Should raise AuthenticationError
        async with AsyncTestingSessionLocal() as session:
            with pytest.raises(AuthenticationError):
                await AuthService.create_user(db=session, user_data=user_data)


class TestAuthServiceTokens:
    """Tests for AuthService token-related methods"""

    @pytest.mark.asyncio
    async def test_create_tokens(self, async_test_user):
        """Test creating access and refresh tokens for a user"""
        tokens = AuthService.create_tokens(async_test_user)

        # Verify token structure
        assert isinstance(tokens, Token)
        assert tokens.access_token is not None
        assert tokens.refresh_token is not None
        assert tokens.token_type == "bearer"

    @pytest.mark.asyncio
    async def test_refresh_tokens(self, async_test_db, async_test_user):
        """Test refreshing tokens with a valid refresh token"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create initial tokens
        initial_tokens = AuthService.create_tokens(async_test_user)

        # Refresh tokens
        async with AsyncTestingSessionLocal() as session:
            new_tokens = await AuthService.refresh_tokens(
                db=session, refresh_token=initial_tokens.refresh_token
            )

            # Verify new tokens are different from old ones
            assert new_tokens.access_token != initial_tokens.access_token
            assert new_tokens.refresh_token != initial_tokens.refresh_token

    @pytest.mark.asyncio
    async def test_refresh_tokens_with_invalid_token(self, async_test_db):
        """Test refreshing tokens with an invalid token"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create an invalid token
        invalid_token = "invalid.token.string"

        # Should raise TokenInvalidError
        async with AsyncTestingSessionLocal() as session:
            with pytest.raises(TokenInvalidError):
                await AuthService.refresh_tokens(
                    db=session, refresh_token=invalid_token
                )

    @pytest.mark.asyncio
    async def test_refresh_tokens_with_access_token(
        self, async_test_db, async_test_user
    ):
        """Test refreshing tokens with an access token instead of refresh token"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create tokens
        tokens = AuthService.create_tokens(async_test_user)

        # Try to refresh with access token
        async with AsyncTestingSessionLocal() as session:
            with pytest.raises(TokenInvalidError):
                await AuthService.refresh_tokens(
                    db=session, refresh_token=tokens.access_token
                )

    @pytest.mark.asyncio
    async def test_refresh_tokens_with_nonexistent_user(self, async_test_db):
        """Test refreshing tokens for a user that doesn't exist in the database"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create a token for a non-existent user
        non_existent_id = str(uuid.uuid4())
        with (
            patch("app.core.auth.decode_token"),
            patch("app.core.auth.get_token_data") as mock_get_data,
        ):
            # Mock the token data to return a non-existent user ID
            mock_get_data.return_value.user_id = uuid.UUID(non_existent_id)

            # Should raise TokenInvalidError
            async with AsyncTestingSessionLocal() as session:
                with pytest.raises(TokenInvalidError):
                    await AuthService.refresh_tokens(
                        db=session, refresh_token="some.refresh.token"
                    )


class TestAuthServicePasswordChange:
    """Tests for AuthService.change_password method"""

    @pytest.mark.asyncio
    async def test_change_password(self, async_test_db, async_test_user):
        """Test changing a user's password"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Set a known password for the mock user
        current_password = "CurrentPassword123"
        async with AsyncTestingSessionLocal() as session:
            result = await session.execute(
                select(User).where(User.id == async_test_user.id)
            )
            user = result.scalar_one_or_none()
            user.password_hash = get_password_hash(current_password)
            await session.commit()

        # Change password
        new_password = "NewPassword456"
        async with AsyncTestingSessionLocal() as session:
            result = await AuthService.change_password(
                db=session,
                user_id=async_test_user.id,
                current_password=current_password,
                new_password=new_password,
            )

            # Verify operation was successful
            assert result is True

        # Verify password was changed
        async with AsyncTestingSessionLocal() as session:
            result = await session.execute(
                select(User).where(User.id == async_test_user.id)
            )
            updated_user = result.scalar_one_or_none()

            # Verify old password no longer works
            assert not verify_password(current_password, updated_user.password_hash)

            # Verify new password works
            assert verify_password(new_password, updated_user.password_hash)

    @pytest.mark.asyncio
    async def test_change_password_wrong_current_password(
        self, async_test_db, async_test_user
    ):
        """Test changing password with incorrect current password"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Set a known password for the mock user
        current_password = "CurrentPassword123"
        async with AsyncTestingSessionLocal() as session:
            result = await session.execute(
                select(User).where(User.id == async_test_user.id)
            )
            user = result.scalar_one_or_none()
            user.password_hash = get_password_hash(current_password)
            await session.commit()

        # Try to change password with wrong current password
        wrong_password = "WrongPassword123"
        async with AsyncTestingSessionLocal() as session:
            with pytest.raises(AuthenticationError):
                await AuthService.change_password(
                    db=session,
                    user_id=async_test_user.id,
                    current_password=wrong_password,
                    new_password="NewPassword456",
                )

        # Verify password was not changed
        async with AsyncTestingSessionLocal() as session:
            result = await session.execute(
                select(User).where(User.id == async_test_user.id)
            )
            user = result.scalar_one_or_none()
            assert verify_password(current_password, user.password_hash)

    @pytest.mark.asyncio
    async def test_change_password_nonexistent_user(self, async_test_db):
        """Test changing password for a user that doesn't exist"""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        non_existent_id = uuid.uuid4()

        async with AsyncTestingSessionLocal() as session:
            with pytest.raises(AuthenticationError):
                await AuthService.change_password(
                    db=session,
                    user_id=non_existent_id,
                    current_password="CurrentPassword123",
                    new_password="NewPassword456",
                )
