"""
Unit tests for ApiKeyService.

Tests API key CRUD operations with mocked persistence layer.

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

from __future__ import annotations

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

from langbot.pkg.api.http.service.apikey import ApiKeyService
from langbot.pkg.entity.persistence.apikey import ApiKey


pytestmark = pytest.mark.asyncio


class TestApiKeyServiceGetApiKeys:
    """Tests for get_api_keys method."""

    async def test_get_api_keys_empty_list(self):
        """Returns empty list when no API keys exist."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_result = Mock()
        mock_result.all = Mock(return_value=[])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)
        ap.persistence_mgr.serialize_model = Mock(
            side_effect=lambda model_cls, entity: {
                'id': entity.id,
                'name': entity.name,
                'key': entity.key,
                'description': entity.description,
            }
            if entity
            else {}
        )

        service = ApiKeyService(ap)

        # Execute
        result = await service.get_api_keys()

        # Verify
        assert result == []
        ap.persistence_mgr.execute_async.assert_called_once()

    async def test_get_api_keys_returns_serialized_list(self):
        """Returns serialized list of API keys."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()

        # Create mock API key entities
        key1 = Mock(spec=ApiKey)
        key1.id = 1
        key1.name = 'Test Key 1'
        key1.key = 'lbk_test_key_1'
        key1.description = 'First test key'

        key2 = Mock(spec=ApiKey)
        key2.id = 2
        key2.name = 'Test Key 2'
        key2.key = 'lbk_test_key_2'
        key2.description = 'Second test key'

        mock_result = Mock()
        mock_result.all = Mock(return_value=[key1, key2])
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)
        ap.persistence_mgr.serialize_model = Mock(
            side_effect=lambda model_cls, entity: {
                'id': entity.id,
                'name': entity.name,
                'key': entity.key,
                'description': entity.description,
            }
        )

        service = ApiKeyService(ap)

        # Execute
        result = await service.get_api_keys()

        # Verify
        assert len(result) == 2
        assert result[0]['name'] == 'Test Key 1'
        assert result[1]['name'] == 'Test Key 2'


class TestApiKeyServiceCreateApiKey:
    """Tests for create_api_key method."""

    async def test_create_api_key_generates_key_with_prefix(self):
        """Creates API key with 'lbk_' prefix."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()

        created_key = Mock(spec=ApiKey)
        created_key.id = 1
        created_key.name = 'New Key'
        created_key.key = 'lbk_fixed-token'
        created_key.description = 'Test description'
        select_result = Mock()
        select_result.first = Mock(return_value=created_key)
        insert_params = []

        async def mock_execute(query):
            params = query.compile().params
            if {'name', 'key', 'description'}.issubset(params):
                insert_params.append(params)
                return Mock()
            return select_result

        ap.persistence_mgr.execute_async = AsyncMock(side_effect=mock_execute)
        ap.persistence_mgr.serialize_model = Mock(
            side_effect=lambda model_cls, entity: {
                'id': 1,
                'name': entity.name,
                'key': entity.key,
                'description': entity.description,
            }
        )

        service = ApiKeyService(ap)

        with patch('langbot.pkg.api.http.service.apikey.secrets.token_urlsafe', return_value='fixed-token'):
            result = await service.create_api_key('New Key', 'Test description')

        assert insert_params == [{'name': 'New Key', 'key': 'lbk_fixed-token', 'description': 'Test description'}]
        assert result['key'].startswith('lbk_')
        assert result['key'] == 'lbk_fixed-token'
        assert result['name'] == 'New Key'
        assert result['description'] == 'Test description'

    async def test_create_api_key_without_description(self):
        """Creates API key with empty description when not provided."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()

        created_key = Mock(spec=ApiKey)
        created_key.id = 1
        created_key.name = 'No Desc Key'
        created_key.key = 'lbk_no_desc_key'
        created_key.description = ''

        select_result = Mock()
        select_result.first = Mock(return_value=created_key)
        insert_result = Mock()

        async def mock_execute(query):
            if hasattr(query, 'values'):
                return insert_result
            return select_result

        ap.persistence_mgr.execute_async = AsyncMock(side_effect=mock_execute)
        ap.persistence_mgr.serialize_model = Mock(
            return_value={
                'id': 1,
                'name': 'No Desc Key',
                'key': 'lbk_no_desc_key',
                'description': '',
            }
        )

        service = ApiKeyService(ap)

        # Execute
        result = await service.create_api_key('No Desc Key')

        # Verify
        assert result['description'] == ''


class TestApiKeyServiceGetApiKey:
    """Tests for get_api_key method."""

    async def test_get_api_key_by_id_found(self):
        """Returns API key when found by ID."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()

        key = Mock(spec=ApiKey)
        key.id = 1
        key.name = 'Found Key'
        key.key = 'lbk_found_key'
        key.description = 'Found'

        mock_result = Mock()
        mock_result.first = Mock(return_value=key)
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)
        ap.persistence_mgr.serialize_model = Mock(
            return_value={
                'id': 1,
                'name': 'Found Key',
                'key': 'lbk_found_key',
                'description': 'Found',
            }
        )

        service = ApiKeyService(ap)

        # Execute
        result = await service.get_api_key(1)

        # Verify
        assert result is not None
        assert result['id'] == 1
        assert result['name'] == 'Found Key'

    async def test_get_api_key_by_id_not_found(self):
        """Returns None when API key not found."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()

        mock_result = Mock()
        mock_result.first = Mock(return_value=None)
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = ApiKeyService(ap)

        # Execute
        result = await service.get_api_key(999)

        # Verify
        assert result is None

    async def test_get_api_key_by_id_zero(self):
        """Handles ID=0 (edge case) correctly."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()

        mock_result = Mock()
        mock_result.first = Mock(return_value=None)
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)

        service = ApiKeyService(ap)

        # Execute
        result = await service.get_api_key(0)

        # Verify - should return None (no key with ID 0)
        assert result is None


class TestApiKeyServiceVerifyApiKey:
    """Tests for verify_api_key method."""

    @staticmethod
    def _make_ap(db_key=None, global_api_key=''):
        """Build a mock Application with persistence + instance_config."""
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_result = Mock()
        mock_result.first = Mock(return_value=db_key)
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)
        ap.instance_config = SimpleNamespace(data={'api': {'global_api_key': global_api_key}})
        return ap

    async def test_verify_api_key_valid(self):
        """Returns True for valid API key."""
        # Setup
        key = Mock(spec=ApiKey)
        ap = self._make_ap(db_key=key)

        service = ApiKeyService(ap)

        # Execute
        result = await service.verify_api_key('lbk_valid_key')

        # Verify
        assert result is True

    async def test_verify_api_key_invalid(self):
        """Returns False for invalid API key."""
        # Setup
        ap = self._make_ap(db_key=None)

        service = ApiKeyService(ap)

        # Execute
        result = await service.verify_api_key('lbk_invalid_key')

        # Verify
        assert result is False

    async def test_verify_api_key_empty_string(self):
        """Returns False for empty key string."""
        # Setup
        ap = self._make_ap(db_key=None)

        service = ApiKeyService(ap)

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

        # Verify
        assert result is False

    async def test_verify_api_key_unknown_key(self):
        """Returns False when the key is not present in persistence."""
        # Setup
        ap = self._make_ap(db_key=None)

        service = ApiKeyService(ap)

        # Execute
        result = await service.verify_api_key('unknown_key')

        # Verify
        assert result is False

    async def test_verify_global_api_key_match(self):
        """Returns True when key matches the config.yaml global API key (no DB lookup)."""
        # Setup: no DB record, but a global key is configured
        ap = self._make_ap(db_key=None, global_api_key='my-global-secret')

        service = ApiKeyService(ap)

        # Execute
        result = await service.verify_api_key('my-global-secret')

        # Verify: accepted purely on config match
        assert result is True
        # DB should not have been consulted for the global-key path
        ap.persistence_mgr.execute_async.assert_not_called()

    async def test_verify_global_api_key_no_prefix_required(self):
        """Global API key is accepted even without the lbk_ prefix."""
        ap = self._make_ap(db_key=None, global_api_key='plainsecret123')

        service = ApiKeyService(ap)

        result = await service.verify_api_key('plainsecret123')

        assert result is True

    async def test_verify_global_api_key_mismatch_falls_back_to_db(self):
        """A non-matching key still falls through to the DB lookup."""
        # Global key set, but request uses a different lbk_ key that IS in DB
        key = Mock(spec=ApiKey)
        ap = self._make_ap(db_key=key, global_api_key='my-global-secret')

        service = ApiKeyService(ap)

        result = await service.verify_api_key('lbk_db_key')

        assert result is True
        ap.persistence_mgr.execute_async.assert_called_once()

    async def test_verify_empty_global_api_key_disabled(self):
        """An empty global_api_key must never authenticate an empty/blank request."""
        ap = self._make_ap(db_key=None, global_api_key='')

        service = ApiKeyService(ap)

        # Empty request key is rejected, and a blank global key never matches
        assert await service.verify_api_key('') is False
        assert await service.verify_api_key('   ') is False

    async def test_verify_api_key_missing_global_config_key(self):
        """Works even when api.global_api_key is absent (existing installs)."""
        # instance_config without the global_api_key field at all
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        mock_result = Mock()
        mock_result.first = Mock(return_value=None)
        ap.persistence_mgr.execute_async = AsyncMock(return_value=mock_result)
        ap.instance_config = SimpleNamespace(data={'api': {}})

        service = ApiKeyService(ap)

        result = await service.verify_api_key('lbk_some_key')

        assert result is False


class TestApiKeyServiceDeleteApiKey:
    """Tests for delete_api_key method."""

    async def test_delete_api_key_by_id(self):
        """Deletes API key by ID."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.persistence_mgr.execute_async = AsyncMock()

        service = ApiKeyService(ap)

        # Execute
        await service.delete_api_key(1)

        # Verify - execute_async was called (delete operation)
        ap.persistence_mgr.execute_async.assert_called_once()

    async def test_delete_api_key_nonexistent_id(self):
        """Delete operation completes even for nonexistent ID (no error raised)."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.persistence_mgr.execute_async = AsyncMock()

        service = ApiKeyService(ap)

        # Execute - should not raise error
        await service.delete_api_key(999)

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


class TestApiKeyServiceUpdateApiKey:
    """Tests for update_api_key method."""

    async def test_update_api_key_name_only(self):
        """Updates only the name field."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.persistence_mgr.execute_async = AsyncMock()

        service = ApiKeyService(ap)

        # Execute
        await service.update_api_key(1, name='Updated Name')

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

    async def test_update_api_key_description_only(self):
        """Updates only the description field."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.persistence_mgr.execute_async = AsyncMock()

        service = ApiKeyService(ap)

        # Execute
        await service.update_api_key(1, description='Updated description')

        # Verify
        ap.persistence_mgr.execute_async.assert_called_once()

    async def test_update_api_key_both_fields(self):
        """Updates both name and description."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.persistence_mgr.execute_async = AsyncMock()

        service = ApiKeyService(ap)

        # Execute
        await service.update_api_key(1, name='New Name', description='New description')

        # Verify
        ap.persistence_mgr.execute_async.assert_called_once()

    async def test_update_api_key_no_fields(self):
        """Does nothing when no fields provided."""
        # Setup
        ap = SimpleNamespace()
        ap.persistence_mgr = SimpleNamespace()
        ap.persistence_mgr.execute_async = AsyncMock()

        service = ApiKeyService(ap)

        # Execute
        await service.update_api_key(1)

        # Verify - no execute call since no update_data
        ap.persistence_mgr.execute_async.assert_not_called()
