"""
Unit tests for UserService.

Tests user management operations including:
- User initialization check
- Local user creation and authentication
- JWT token generation and verification
- Password management (reset, change, set)
- Space account management

Source: src/langbot/pkg/api/http/service/user.py
"""

from __future__ import annotations

import pytest
from unittest.mock import AsyncMock, Mock
from types import SimpleNamespace

from langbot.pkg.api.http.service.user import UserService
from langbot.pkg.entity.persistence.user import User
from langbot.pkg.entity.errors.account import AccountEmailMismatchError


pytestmark = pytest.mark.asyncio


def _create_mock_user(
    email: str = 'test@example.com',
    password: str = 'hashed_password',
    account_type: str = 'local',
    space_account_uuid: str = None,
) -> Mock:
    """Helper to create mock User entity."""
    user = Mock(spec=User)
    user.user = email
    user.password = password
    user.account_type = account_type
    user.space_account_uuid = space_account_uuid
    return user


def _create_mock_result(items: list = None, first_item=None):
    """Create mock result object for persistence queries."""
    result = Mock()
    result.all = Mock(return_value=items or [])
    result.first = Mock(return_value=first_item)
    return result


class TestUserServiceIsInitialized:
    """Tests for is_initialized method."""

    async def test_is_initialized_returns_true_when_users_exist(self):
        """Returns True when at least one user exists."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_user = _create_mock_user()
        mock_result = _create_mock_result([mock_user])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute
        result = await service.is_initialized()

        # Verify
        assert result is True

    async def test_is_initialized_returns_false_when_no_users(self):
        """Returns False when no users exist."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_result = _create_mock_result([])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute
        result = await service.is_initialized()

        # Verify
        assert result is False

    async def test_is_initialized_returns_false_on_none_result(self):
        """Returns False when result is None."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_result = Mock()
        mock_result.all = Mock(return_value=None)
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute
        result = await service.is_initialized()

        # Verify
        assert result is False


class TestUserServiceGetUserByEmail:
    """Tests for get_user_by_email method."""

    async def test_get_user_by_email_found(self):
        """Returns user when found."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_user = _create_mock_user(email='found@example.com')
        mock_result = _create_mock_result([mock_user])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute
        result = await service.get_user_by_email('found@example.com')

        # Verify
        assert result is not None
        assert result.user == 'found@example.com'

    async def test_get_user_by_email_not_found(self):
        """Returns None when user not found."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_result = _create_mock_result([])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute
        result = await service.get_user_by_email('notfound@example.com')

        # Verify
        assert result is None

    async def test_get_user_by_email_empty_string(self):
        """Handles empty email string."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_result = _create_mock_result([])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute
        result = await service.get_user_by_email('')

        # Verify
        assert result is None


class TestUserServiceGetUserBySpaceAccountUuid:
    """Tests for get_user_by_space_account_uuid method."""

    async def test_get_user_by_space_uuid_found(self):
        """Returns user when Space UUID found."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_user = _create_mock_user(
            email='space@example.com',
            account_type='space',
            space_account_uuid='space-uuid-123',
        )
        mock_result = _create_mock_result([mock_user])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute
        result = await service.get_user_by_space_account_uuid('space-uuid-123')

        # Verify
        assert result is not None
        assert result.space_account_uuid == 'space-uuid-123'

    async def test_get_user_by_space_uuid_not_found(self):
        """Returns None when Space UUID not found."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_result = _create_mock_result([])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute
        result = await service.get_user_by_space_account_uuid('nonexistent-uuid')

        # Verify
        assert result is None


class TestUserServiceAuthenticate:
    """Tests for authenticate method."""

    async def test_authenticate_user_not_found_raises_error(self):
        """Raises ValueError when user not found."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_result = _create_mock_result([])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)
        ap.instance_config = SimpleNamespace()
        ap.instance_config.data = {'system': {'jwt': {'secret': 'test_secret', 'expire': 3600}}}

        service = UserService(ap)

        # Execute & Verify
        with pytest.raises(ValueError, match='用户不存在'):
            await service.authenticate('nonexistent@example.com', 'password')

    async def test_authenticate_space_user_without_password_raises_error(self):
        """Raises ValueError for Space user without local password."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        # Space user has empty password
        mock_user = _create_mock_user(
            email='space@example.com',
            password='',  # Empty password for Space user
            account_type='space',
        )
        mock_result = _create_mock_result([mock_user])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute & Verify
        with pytest.raises(ValueError, match='请使用 Space 账户登录'):
            await service.authenticate('space@example.com', 'password')


class TestUserServiceGenerateJwtToken:
    """Tests for generate_jwt_token method."""

    async def test_generate_jwt_token_returns_valid_token(self):
        """Generates valid JWT token."""
        # Setup
        ap = SimpleNamespace()
        ap.instance_config = SimpleNamespace()
        ap.instance_config.data = {'system': {'jwt': {'secret': 'test_secret', 'expire': 3600}}}

        service = UserService(ap)

        # Execute
        token = await service.generate_jwt_token('test@example.com')

        # Verify - JWT format (base64 encoded parts)
        assert token is not None
        assert len(token) > 0
        parts = token.split('.')
        assert len(parts) == 3  # JWT has 3 parts

    async def test_generate_jwt_token_custom_expire(self):
        """Generates token with custom expiry."""
        # Setup
        ap = SimpleNamespace()
        ap.instance_config = SimpleNamespace()
        ap.instance_config.data = {'system': {'jwt': {'secret': 'test_secret', 'expire': 7200}}}

        service = UserService(ap)

        # Execute
        token = await service.generate_jwt_token('test@example.com')

        # Verify
        assert token is not None


class TestUserServiceVerifyJwtToken:
    """Tests for verify_jwt_token method."""

    async def test_verify_jwt_token_valid(self):
        """Verifies valid JWT token and returns user email."""
        # Setup
        ap = SimpleNamespace()
        ap.instance_config = SimpleNamespace()
        ap.instance_config.data = {'system': {'jwt': {'secret': 'test_secret', 'expire': 3600}}}

        service = UserService(ap)

        # First generate a valid token
        token = await service.generate_jwt_token('verify@example.com')

        # Execute
        user_email = await service.verify_jwt_token(token)

        # Verify
        assert user_email == 'verify@example.com'

    async def test_verify_jwt_token_invalid_raises_error(self):
        """Raises error for invalid JWT token."""
        # Setup
        ap = SimpleNamespace()
        ap.instance_config = SimpleNamespace()
        ap.instance_config.data = {'system': {'jwt': {'secret': 'test_secret', 'expire': 3600}}}

        service = UserService(ap)

        # Execute & Verify - invalid token should raise JWT error
        with pytest.raises(Exception):  # jwt.DecodeError or similar
            await service.verify_jwt_token('invalid.token.here')


class TestUserServiceResetPassword:
    """Tests for reset_password method."""

    async def test_reset_password_updates_password(self):
        """Updates user password."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.persistence_mgr.execute_async = AsyncMock()

        service = UserService(ap)

        # Execute
        await service.reset_password('test@example.com', 'new_password')

        # Verify - execute_async was called with update
        ap.persistence_mgr.execute_async.assert_called_once()


class TestUserServiceChangePassword:
    """Tests for change_password method."""

    async def test_change_password_user_not_found_raises_error(self):
        """Raises ValueError when user not found."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()

        service = UserService(ap)

        # Mock get_user_by_email to return None
        service.get_user_by_email = AsyncMock(return_value=None)

        # Execute & Verify
        with pytest.raises(ValueError, match='User not found'):
            await service.change_password('nonexistent@example.com', 'current', 'new')

    async def test_change_password_no_local_password_raises_error(self):
        """Raises ValueError when user has no local password set."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()

        service = UserService(ap)

        # Mock user without password
        mock_user = _create_mock_user(email='nopass@example.com', password=None)
        service.get_user_by_email = AsyncMock(return_value=mock_user)

        # Execute & Verify
        with pytest.raises(ValueError, match='No local password set'):
            await service.change_password('nopass@example.com', 'current', 'new')


class TestUserServiceGetFirstUser:
    """Tests for get_first_user method."""

    async def test_get_first_user_found(self):
        """Returns first user when exists."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_user = _create_mock_user(email='first@example.com')
        mock_result = _create_mock_result([mock_user])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute
        result = await service.get_first_user()

        # Verify
        assert result is not None
        assert result.user == 'first@example.com'

    async def test_get_first_user_not_found(self):
        """Returns None when no users exist."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_result = _create_mock_result([])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = UserService(ap)

        # Execute
        result = await service.get_first_user()

        # Verify
        assert result is None


class TestUserServiceSetPassword:
    """Tests for set_password method."""

    async def test_set_password_user_not_found_raises_error(self):
        """Raises ValueError when user not found."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()

        service = UserService(ap)

        # Mock get_user_by_email to return None
        service.get_user_by_email = AsyncMock(return_value=None)

        # Execute & Verify
        with pytest.raises(ValueError, match='User not found'):
            await service.set_password('nonexistent@example.com', 'new_password')

    async def test_set_password_with_existing_password_requires_current(self):
        """Requires current password when user has existing password."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()

        service = UserService(ap)

        # Mock user with existing password
        mock_user = _create_mock_user(email='haspass@example.com', password='hashed_old_password')
        service.get_user_by_email = AsyncMock(return_value=mock_user)

        # Execute & Verify - should raise when no current_password provided
        with pytest.raises(ValueError, match='Current password is required'):
            await service.set_password('haspass@example.com', 'new_password')


class TestUserServiceCreateOrUpdateSpaceUser:
    """Tests for create_or_update_space_user method."""

    async def test_create_or_update_existing_space_user(self):
        """Updates existing Space user tokens."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.provider_service = SimpleNamespace()
        ap.provider_service.update_space_model_provider_api_keys = AsyncMock()

        service = UserService(ap)

        # Mock existing Space user
        existing_user = _create_mock_user(
            email='space@example.com',
            account_type='space',
            space_account_uuid='existing-space-uuid',
        )
        service.get_user_by_space_account_uuid = AsyncMock(return_value=existing_user)
        service.get_user_by_email = AsyncMock(return_value=None)
        service.is_initialized = AsyncMock(return_value=True)

        ap.persistence_mgr.execute_async = AsyncMock()

        # Execute
        updated_user = await service.create_or_update_space_user(
            space_account_uuid='existing-space-uuid',
            email='space@example.com',
            access_token='new_access_token',
            refresh_token='new_refresh_token',
            api_key='new_api_key',
            expires_in=3600,
        )

        # Verify - update was called and user returned
        ap.persistence_mgr.execute_async.assert_called()
        assert updated_user.space_account_uuid == 'existing-space-uuid'

    async def test_create_or_update_new_space_user_first_init(self):
        """Creates new Space user on first initialization."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.provider_service = SimpleNamespace()
        ap.provider_service.update_space_model_provider_api_keys = AsyncMock()

        service = UserService(ap)

        # Mock new user to be returned after creation
        new_user = _create_mock_user(
            email='newspace@example.com',
            account_type='space',
            space_account_uuid='new-space-uuid',
        )

        # First call (line 138) returns None, second call (line 194) returns new_user
        call_count = 0

        async def mock_get_by_space_uuid(uuid):
            nonlocal call_count
            call_count += 1
            if call_count == 1:  # First check for existing user
                return None
            return new_user  # After insert, return the new user

        service.get_user_by_space_account_uuid = AsyncMock(side_effect=mock_get_by_space_uuid)
        service.get_user_by_email = AsyncMock(return_value=None)
        service.is_initialized = AsyncMock(return_value=False)  # Not initialized

        ap.persistence_mgr.execute_async = AsyncMock()

        # Execute
        result = await service.create_or_update_space_user(
            space_account_uuid='new-space-uuid',
            email='newspace@example.com',
            access_token='access_token',
            refresh_token='refresh_token',
            api_key='api_key',
            expires_in=3600,
        )

        # Verify
        assert result.space_account_uuid == 'new-space-uuid'

    async def test_create_or_update_space_user_already_initialized_raises_error(self):
        """Raises AccountEmailMismatchError when system already initialized and user not found."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.provider_service = SimpleNamespace()
        ap.provider_service.update_space_model_provider_api_keys = AsyncMock()

        service = UserService(ap)

        # Mock system already initialized, no matching users
        service.get_user_by_space_account_uuid = AsyncMock(return_value=None)
        service.get_user_by_email = AsyncMock(return_value=None)
        service.is_initialized = AsyncMock(return_value=True)  # Already initialized

        # Execute & Verify
        with pytest.raises(AccountEmailMismatchError):
            await service.create_or_update_space_user(
                space_account_uuid='unknown-space-uuid',
                email='unknown@example.com',
                access_token='token',
                refresh_token='refresh',
                api_key='key',
                expires_in=3600,
            )

    async def test_create_or_update_space_user_no_expiry(self):
        """Creates Space user without token expiry."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.provider_service = SimpleNamespace()
        ap.provider_service.update_space_model_provider_api_keys = AsyncMock()

        service = UserService(ap)

        new_user = _create_mock_user(
            email='noexpiry@example.com',
            account_type='space',
            space_account_uuid='noexpiry-uuid',
        )

        # First call (line 138) returns None, second call (line 194) returns new_user
        call_count = 0

        async def mock_get_by_space_uuid(uuid):
            nonlocal call_count
            call_count += 1
            if call_count == 1:  # First check for existing user
                return None
            return new_user  # After insert, return the new user

        service.get_user_by_space_account_uuid = AsyncMock(side_effect=mock_get_by_space_uuid)
        service.get_user_by_email = AsyncMock(return_value=None)
        service.is_initialized = AsyncMock(return_value=False)

        ap.persistence_mgr.execute_async = AsyncMock()

        # Execute with expires_in=0 (no expiry)
        result = await service.create_or_update_space_user(
            space_account_uuid='noexpiry-uuid',
            email='noexpiry@example.com',
            access_token='token',
            refresh_token='refresh',
            api_key='key',
            expires_in=0,  # No expiry
        )

        # Verify
        assert result is not None
        assert result.space_account_uuid == 'noexpiry-uuid'


class TestUserServiceCreateUserLock:
    """Tests for create_user_lock attribute."""

    def test_create_user_lock_initialized(self):
        """Verify create_user_lock is initialized as asyncio.Lock."""
        # Setup
        ap = SimpleNamespace()

        service = UserService(ap)

        # Verify lock exists
        assert hasattr(service, '_create_user_lock')
        assert service._create_user_lock is not None
