import sys
import pytest
import os
import tempfile
import shutil
import time
from pathlib import Path
from typing import List, Dict, Any, Optional, Union
from unittest.mock import Mock, patch, AsyncMock

# Skip entire module on Python 3.14+ due to pydantic compatibility issues
if sys.version_info >= (3, 14):
    pytest.skip(
        "pydantic 2.12.5 is not compatible with Python 3.14 (typing._eval_type API change)",
        allow_module_level=True
    )

from upsonic.embeddings.base import EmbeddingProvider, EmbeddingConfig, EmbeddingMode, EmbeddingMetrics
from upsonic.embeddings.openai_provider import OpenAIEmbedding, OpenAIEmbeddingConfig
from upsonic.embeddings.azure_openai_provider import AzureOpenAIEmbedding, AzureOpenAIEmbeddingConfig
from upsonic.embeddings.bedrock_provider import BedrockEmbedding, BedrockEmbeddingConfig
from upsonic.embeddings.ollama_provider import OllamaEmbedding, OllamaEmbeddingConfig
from upsonic.embeddings.gemini_provider import GeminiEmbedding, GeminiEmbeddingConfig
from upsonic.embeddings.factory import create_embedding_provider, list_available_providers

try:
    from upsonic.embeddings.huggingface_provider import HuggingFaceEmbedding, HuggingFaceEmbeddingConfig
except (ImportError, ModuleNotFoundError):
    HuggingFaceEmbedding = None
    HuggingFaceEmbeddingConfig = None

try:
    from upsonic.embeddings.fastembed_provider import FastEmbedProvider, FastEmbedConfig
except (ImportError, ModuleNotFoundError):
    FastEmbedProvider = None
    FastEmbedConfig = None

from upsonic.knowledge_base.knowledge_base import KnowledgeBase
from upsonic.vectordb.providers.faiss import FaissProvider
from upsonic.vectordb.config import DistanceMetric
from upsonic.schemas.data_models import Chunk, Document
from upsonic.text_splitter.base import BaseChunker
from upsonic.loaders.base import BaseLoader

from upsonic.utils.package.exception import ConfigurationError, ModelConnectionError


class MockEmbeddingProvider(EmbeddingProvider):
    """Mock embedding provider for testing without API dependencies."""
    
    def __init__(self, config: Optional[EmbeddingConfig] = None, **kwargs):
        if config is None:
            config = EmbeddingConfig(model_name="mock-embedding-model", **kwargs)
        super().__init__(config=config)
        self._model_info = {
            "model_name": "mock-embedding-model",
            "provider": "Mock",
            "type": "embedding",
            "dimensions": 384,
            "max_tokens": 512
        }
    
    async def _embed_batch(self, texts: List[str], mode: EmbeddingMode = EmbeddingMode.DOCUMENT) -> List[List[float]]:
        """Generate mock embeddings."""
        if not texts:
            return []
        
        # Generate deterministic embeddings based on text content
        embeddings = []
        for text in texts:
            # Use a more stable hash function for deterministic results
            import hashlib
            text_hash = int(hashlib.md5(text.encode()).hexdigest()[:8], 16) % 1000
            embedding = [float((text_hash + i) % 100) / 100.0 for i in range(384)]
            embeddings.append(embedding)
        
        return embeddings
    
    def get_model_info(self) -> Dict[str, Any]:
        return self._model_info
    
    @property
    def supported_modes(self) -> List[EmbeddingMode]:
        return [EmbeddingMode.DOCUMENT, EmbeddingMode.QUERY, EmbeddingMode.SYMMETRIC]
    
    @property
    def pricing_info(self) -> Dict[str, float]:
        return {
            "per_million_tokens": 0.0,
            "currency": "USD",
            "note": "Mock provider - no costs"
        }


class MockChunker(BaseChunker):
    """Mock text chunker for testing."""
    
    def __init__(self, chunk_size: int = 100, chunk_overlap: int = 20):
        self.chunk_size = chunk_size
        self.chunk_overlap = chunk_overlap
    
    def chunk_text(self, text: str) -> List[str]:
        """Simple chunking implementation."""
        if len(text) <= self.chunk_size:
            return [text]
        
        chunks = []
        start = 0
        while start < len(text):
            end = min(start + self.chunk_size, len(text))
            chunk = text[start:end]
            chunks.append(chunk)
            start = end - self.chunk_overlap
            if start >= len(text):
                break
        
        return chunks
    
    def _chunk_document(self, document: Document) -> List[Chunk]:
        """Implement the abstract method for document chunking."""
        text_chunks = self.chunk_text(document.content)
        chunks = []
        for i, chunk_text in enumerate(text_chunks):
            chunk = Chunk(
                text_content=chunk_text,
                metadata={
                    "chunk_index": i,
                    "source": document.metadata.get("source", "unknown"),
                    "total_chunks": len(text_chunks)
                }
            )
            chunks.append(chunk)
        return chunks


class MockLoader(BaseLoader):
    """Mock document loader for testing."""
    
    def __init__(self, content: str = "Sample document content for testing."):
        self.content = content
    
    def load(self, source: Union[str, Path]) -> List[Document]:
        """Load mock document."""
        return [Document(
            content=self.content,
            metadata={"source": str(source), "type": "mock"},
            chunks=[]
        )]
    
    def batch(self, sources: List[Union[str, Path]]) -> List[Document]:
        """Load multiple mock documents."""
        return [self.load(source) for source in sources]
    
    async def aload(self, source: Union[str, Path]) -> List[Document]:
        """Async load mock document."""
        return self.load(source)
    
    async def abatch(self, sources: List[Union[str, Path]]) -> List[Document]:
        """Async load multiple mock documents."""
        return self.batch(sources)
    
    def get_supported_extensions(self) -> List[str]:
        """Get supported file extensions."""
        return [".txt", ".md", ".mock"]


class TestEmbeddingProviders:
    """Test suite for all embedding providers."""
    
    @pytest.fixture
    def mock_openai_client(self):
        """Mock OpenAI client."""
        mock_client = AsyncMock()
        mock_response = Mock()
        mock_response.data = [Mock(embedding=[0.1] * 1536) for _ in range(3)]
        mock_response.usage = Mock(total_tokens=100)
        mock_client.embeddings.create.return_value = mock_response
        return mock_client
    
    @pytest.fixture
    def mock_azure_client(self):
        """Mock Azure OpenAI client."""
        mock_client = AsyncMock()
        mock_response = Mock()
        mock_response.data = [Mock(embedding=[0.1] * 1536) for _ in range(3)]
        mock_response.usage = Mock(total_tokens=100)
        mock_client.embeddings.create.return_value = mock_response
        return mock_client
    
    @pytest.fixture
    def mock_bedrock_client(self):
        """Mock AWS Bedrock client."""
        mock_client = Mock()
        mock_response = {
            'body': Mock(read=Mock(return_value=b'{"embedding": [0.1] * 1536}'))
        }
        mock_client.invoke_model.return_value = mock_response
        return mock_client
    
    @pytest.fixture
    def mock_gemini_client(self):
        """Mock Gemini client."""
        mock_client = Mock()
        mock_response = Mock()
        mock_response.embeddings = [Mock(values=[0.1] * 768)]
        mock_client.models.embed_content.return_value = mock_response
        return mock_client
    
    @pytest.fixture
    def mock_ollama_response(self):
        """Mock Ollama API response."""
        return {"embedding": [0.1] * 384}
    
    @pytest.fixture
    def mock_huggingface_model(self):
        """Mock HuggingFace model."""
        mock_model = Mock()
        mock_tokenizer = Mock()
        
        # Mock tokenizer behavior
        mock_tokenizer.return_value = {
            'input_ids': Mock(),
            'attention_mask': Mock()
        }
        mock_tokenizer.model_max_length = 512
        
        # Mock model behavior
        mock_output = Mock()
        mock_output.__getitem__ = Mock(return_value=Mock())  # last_hidden_state
        mock_model.return_value = mock_output
        
        return mock_model, mock_tokenizer
    
    @pytest.fixture
    def mock_fastembed_model(self):
        """Mock FastEmbed model."""
        mock_model = Mock()
        mock_embeddings = [[0.1] * 384 for _ in range(3)]
        mock_model.embed.return_value = iter(mock_embeddings)
        return mock_model
    
    def test_openai_embedding_creation(self, mock_openai_client):
        """Test OpenAI embedding provider creation and basic functionality."""
        with patch('upsonic.embeddings.openai_provider.AsyncOpenAI', return_value=mock_openai_client):
            with patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}):
                config = OpenAIEmbeddingConfig(
                    model_name="text-embedding-3-small",
                    api_key="test-key"
                )
                provider = OpenAIEmbedding(config=config)
                
                assert provider.config.model_name == "text-embedding-3-small"
                assert provider.supported_modes == [EmbeddingMode.DOCUMENT, EmbeddingMode.QUERY]
                assert provider.pricing_info["per_million_tokens"] == 0.02
    
    def test_azure_openai_embedding_creation(self, mock_azure_client):
        """Test Azure OpenAI embedding provider creation."""
        with patch('upsonic.embeddings.azure_openai_provider.AsyncAzureOpenAI', return_value=mock_azure_client):
            config = AzureOpenAIEmbeddingConfig(
                model_name="text-embedding-3-small",
                api_key="test-key",
                azure_endpoint="https://test.openai.azure.com/",
                deployment_name="test-deployment"
            )
            provider = AzureOpenAIEmbedding(config=config)
            
            assert provider.config.model_name == "text-embedding-3-small"
            assert provider.config.azure_endpoint == "https://test.openai.azure.com/"
            assert provider.supported_modes == [EmbeddingMode.DOCUMENT, EmbeddingMode.QUERY]
    
    def test_bedrock_embedding_creation(self, mock_bedrock_client):
        """Test AWS Bedrock embedding provider creation."""
        with patch('boto3.Session') as mock_session:
            mock_session.return_value.client.return_value = mock_bedrock_client
            
            config = BedrockEmbeddingConfig(
                model_name="amazon.titan-embed-text-v1",
                region_name="us-east-1",
                aws_access_key_id="test-key",
                aws_secret_access_key="test-secret"
            )
            provider = BedrockEmbedding(config=config)
            
            assert provider.config.model_name == "amazon.titan-embed-text-v1"
            assert provider.supported_modes == [EmbeddingMode.DOCUMENT, EmbeddingMode.QUERY]
    
    def test_gemini_embedding_creation(self, mock_gemini_client):
        """Test Google Gemini embedding provider creation."""
        with patch('google.genai.Client', return_value=mock_gemini_client):
            with patch.dict(os.environ, {'GOOGLE_API_KEY': 'test-key'}):
                config = GeminiEmbeddingConfig(
                    model_name="gemini-embedding-001",
                    api_key="test-key"
                )
                provider = GeminiEmbedding(config=config)
                
                assert provider.config.model_name == "gemini-embedding-001"
                assert EmbeddingMode.DOCUMENT in provider.supported_modes
                assert EmbeddingMode.QUERY in provider.supported_modes
    
    def test_ollama_embedding_creation(self, mock_ollama_response):
        """Test Ollama embedding provider creation."""
        with patch('aiohttp.ClientSession') as mock_session:
            mock_response = AsyncMock()
            mock_response.status = 200
            mock_response.json.return_value = mock_ollama_response
            mock_session.return_value.__aenter__.return_value.post.return_value.__aenter__.return_value = mock_response
            
            config = OllamaEmbeddingConfig(
                model_name="nomic-embed-text",
                base_url="http://localhost:11434"
            )
            provider = OllamaEmbedding(config=config)
            
            assert provider.config.model_name == "nomic-embed-text"
            assert provider.supported_modes == [EmbeddingMode.DOCUMENT, EmbeddingMode.QUERY, EmbeddingMode.SYMMETRIC]
    
    def test_huggingface_embedding_creation(self, mock_huggingface_model):
        """Test HuggingFace embedding provider creation."""
        if HuggingFaceEmbedding is None or HuggingFaceEmbeddingConfig is None:
            pytest.skip("huggingface provider dependencies (e.g. numpy) not installed")
        mock_model, mock_tokenizer = mock_huggingface_model
        
        with patch('transformers.AutoModel.from_pretrained', return_value=mock_model):
            with patch('transformers.AutoTokenizer.from_pretrained', return_value=mock_tokenizer):
                config = HuggingFaceEmbeddingConfig(
                    model_name="sentence-transformers/all-MiniLM-L6-v2",
                    use_local=True,
                    device="cpu"
                )
                provider = HuggingFaceEmbedding(config=config)
                
                assert provider.config.model_name == "sentence-transformers/all-MiniLM-L6-v2"
                assert provider.supported_modes == [EmbeddingMode.DOCUMENT, EmbeddingMode.QUERY, EmbeddingMode.SYMMETRIC]
    
    def test_fastembed_embedding_creation(self, mock_fastembed_model):
        """Test FastEmbed provider creation."""
        if FastEmbedProvider is None or FastEmbedConfig is None:
            pytest.skip("fastembed provider not installed")
        pytest.importorskip("fastembed", reason="fastembed requires onnxruntime which is not available on Python <3.11")
        with patch('upsonic.embeddings.fastembed_provider.TextEmbedding', return_value=mock_fastembed_model):
            config = FastEmbedConfig(
                model_name="BAAI/bge-small-en-v1.5"
            )
            provider = FastEmbedProvider(config=config)
            
            assert provider.config.model_name == "BAAI/bge-small-en-v1.5"
            assert provider.supported_modes == [EmbeddingMode.DOCUMENT, EmbeddingMode.QUERY, EmbeddingMode.SYMMETRIC]
    
    @pytest.mark.asyncio
    async def test_embedding_batch_processing(self):
        """Test batch processing for embeddings."""
        provider = MockEmbeddingProvider()
        try:
            texts = ["Hello world", "Test embedding", "Another test"]
            embeddings = await provider.embed_texts(texts)
            
            assert len(embeddings) == 3
            assert all(len(emb) == 384 for emb in embeddings)
            assert all(isinstance(emb, list) for emb in embeddings)
        finally:
            if hasattr(provider, 'close'):
                await provider.close()
    
    @pytest.mark.asyncio
    async def test_embedding_caching(self):
        """Test embedding caching functionality."""
        config = EmbeddingConfig(
            cache_embeddings=True,
            model_name="test-model"
        )
        provider = MockEmbeddingProvider(config=config)
        try:
            texts = ["Cached text", "Another cached text"]
            
            # First call - should compute embeddings
            embeddings1 = await provider.embed_texts(texts)
            
            # Second call - should use cache
            embeddings2 = await provider.embed_texts(texts)
            
            # Test that caching is enabled and embeddings are returned
            assert len(embeddings1) == len(embeddings2)
            assert len(embeddings1) == 2
            assert provider.get_cache_info()["enabled"] is True
        finally:
            if hasattr(provider, 'close'):
                await provider.close()
    
    @pytest.mark.asyncio
    async def test_embedding_validation(self):
        """Test embedding connection validation."""
        provider = MockEmbeddingProvider()
        try:
            is_valid = await provider.validate_connection()
            assert is_valid is True
        finally:
            if hasattr(provider, 'close'):
                await provider.close()
    
    def test_embedding_metrics(self):
        """Test embedding metrics collection."""
        provider = MockEmbeddingProvider()
        
        metrics = provider.get_metrics()
        assert isinstance(metrics, EmbeddingMetrics)
        assert metrics.provider == "MockEmbeddingProvider"
        assert metrics.model_name == "mock-embedding-model"
    
    def test_embedding_cost_estimation(self):
        """Test cost estimation functionality."""
        provider = MockEmbeddingProvider()
        
        cost_estimate = provider.estimate_cost(num_texts=100, avg_text_length=50)
        assert "estimated_cost" in cost_estimate
        assert "estimated_tokens" in cost_estimate


class TestKnowledgeBaseIntegration:
    """Test suite for knowledge base integration with embedding providers."""
    
    @pytest.fixture
    def temp_dir(self):
        """Create temporary directory for testing."""
        temp_dir = tempfile.mkdtemp()
        yield temp_dir
        shutil.rmtree(temp_dir)
    
    @pytest.fixture
    def mock_embedding_provider(self):
        """Create mock embedding provider."""
        return MockEmbeddingProvider()
    
    @pytest.fixture
    def mock_vectordb_config(self, temp_dir):
        """Create mock vector database configuration."""
        from upsonic.vectordb.config import FaissConfig, FlatIndexConfig
        
        return FaissConfig(
            db_path=temp_dir,
            collection_name="test_collection",
            vector_size=384,
            distance_metric=DistanceMetric.COSINE,
            index=FlatIndexConfig(),
            default_top_k=10,
            default_similarity_threshold=0.7,
            provider_id="test_faiss_provider_id"
        )
    
    @pytest.fixture
    def mock_vectordb(self, mock_vectordb_config):
        """Create mock vector database provider."""
        try:
            import faiss
            return FaissProvider(mock_vectordb_config)
        except ImportError:
            pytest.skip("faiss-cpu not available")
    
    @pytest.fixture
    def sample_documents(self):
        """Create sample documents for testing."""
        return [
            Document(
                content="This is a test document about artificial intelligence and machine learning.",
                metadata={"source": "test1.txt", "type": "text"},
                chunks=[]
            ),
            Document(
                content="Another document about natural language processing and embeddings.",
                metadata={"source": "test2.txt", "type": "text"},
                chunks=[]
            )
        ]
    
    def test_knowledge_base_creation(self, mock_embedding_provider, mock_vectordb, temp_dir):
        """Test knowledge base creation with embedding provider."""
        chunker = MockChunker(chunk_size=50, chunk_overlap=10)
        loader = MockLoader("Sample content for testing knowledge base.")
        
        kb = KnowledgeBase(
            sources=temp_dir,
            embedding_provider=mock_embedding_provider,
            vectordb=mock_vectordb,
            splitters=chunker,
            loaders=loader,
            name="test_kb"
        )
        
        assert kb.name == "test_kb"
        assert kb.embedding_provider == mock_embedding_provider
        assert kb.vectordb == mock_vectordb
    
    @pytest.mark.asyncio
    async def test_knowledge_base_setup(self, mock_embedding_provider, mock_vectordb, temp_dir):
        """Test knowledge base setup process."""
        chunker = MockChunker(chunk_size=50, chunk_overlap=10)
        loader = MockLoader("Sample content for testing knowledge base setup.")
        
        kb = KnowledgeBase(
            sources=temp_dir,
            embedding_provider=mock_embedding_provider,
            vectordb=mock_vectordb,
            splitters=chunker,
            loaders=loader
        )
        try:
            # Test setup process
            await kb.setup_async()
            
            # Verify setup completed (check if knowledge base is ready instead of vectordb)
            assert kb._is_ready is True
        finally:
            if hasattr(kb, 'close'):
                await kb.close()
    
    @pytest.mark.asyncio
    async def test_knowledge_base_query(self, mock_embedding_provider, mock_vectordb, temp_dir):
        """Test knowledge base querying functionality."""
        chunker = MockChunker(chunk_size=50, chunk_overlap=10)
        loader = MockLoader("Sample content for testing knowledge base queries.")
        
        kb = KnowledgeBase(
            sources=temp_dir,
            embedding_provider=mock_embedding_provider,
            vectordb=mock_vectordb,
            splitters=chunker,
            loaders=loader
        )
        try:
            # Setup knowledge base
            await kb.setup_async()
            
            # Test query - handle case where setup might not complete fully
            try:
                results = await kb.query_async("test query")
                assert isinstance(results, list)
            except Exception as e:
                # If query fails due to setup not completing, that's acceptable for this test
                # The important thing is that the method can be called without crashing
                assert "not ready" in str(e).lower() or "index" in str(e).lower()
        finally:
            if hasattr(kb, 'close'):
                await kb.close()
    
    @pytest.mark.asyncio
    async def test_knowledge_base_health_check(self, mock_embedding_provider, mock_vectordb, temp_dir):
        """Test knowledge base health check."""
        chunker = MockChunker(chunk_size=50, chunk_overlap=10)
        loader = MockLoader("Sample content for testing health check.")
        
        kb = KnowledgeBase(
            sources=temp_dir,
            embedding_provider=mock_embedding_provider,
            vectordb=mock_vectordb,
            splitters=chunker,
            loaders=loader
        )
        try:
            health_status = await kb.health_check_async()
            
            assert isinstance(health_status, dict)
            assert "healthy" in health_status
            assert "components" in health_status
        finally:
            if hasattr(kb, 'close'):
                await kb.close()
    
    def test_knowledge_base_config_summary(self, mock_embedding_provider, mock_vectordb, temp_dir):
        """Test knowledge base configuration summary."""
        chunker = MockChunker(chunk_size=50, chunk_overlap=10)
        loader = MockLoader("Sample content for testing config summary.")
        
        kb = KnowledgeBase(
            sources=temp_dir,
            embedding_provider=mock_embedding_provider,
            vectordb=mock_vectordb,
            splitters=chunker,
            loaders=loader,
            name="test_kb"
        )
        
        config_summary = kb.get_config_summary()
        
        assert isinstance(config_summary, dict)
        assert "knowledge_base" in config_summary
        assert "embedding_provider" in config_summary
        assert "vectordb" in config_summary


class TestEmbeddingProviderFactory:
    """Test suite for embedding provider factory functions."""
    
    def test_list_available_providers(self):
        """Test listing available providers."""
        providers = list_available_providers()
        
        assert isinstance(providers, list)
        assert len(providers) > 0
        # Should include at least some providers
        expected_providers = ["openai", "huggingface", "fastembed", "ollama", "gemini"]
        for provider in expected_providers:
            assert provider in providers
    
    def test_create_embedding_provider_openai(self):
        """Test creating OpenAI embedding provider via factory."""
        with patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}):
            with patch('upsonic.embeddings.openai_provider.AsyncOpenAI'):
                provider = create_embedding_provider("openai", model_name="text-embedding-3-small")
                
                assert isinstance(provider, OpenAIEmbedding)
                assert provider.config.model_name == "text-embedding-3-small"
    
    def test_create_embedding_provider_huggingface(self):
        """Test creating HuggingFace embedding provider via factory."""
        if HuggingFaceEmbedding is None:
            pytest.skip("huggingface provider dependencies (e.g. numpy) not installed")
        with patch('transformers.AutoModel.from_pretrained'):
            with patch('transformers.AutoTokenizer.from_pretrained'):
                provider = create_embedding_provider(
                    "huggingface", 
                    model_name="sentence-transformers/all-MiniLM-L6-v2"
                )
                
                assert isinstance(provider, HuggingFaceEmbedding)
                assert provider.config.model_name == "sentence-transformers/all-MiniLM-L6-v2"
    
    def test_create_embedding_provider_fastembed(self):
        """Test creating FastEmbed provider via factory."""
        if FastEmbedProvider is None:
            pytest.skip("fastembed provider not installed")
        pytest.importorskip("fastembed", reason="fastembed requires onnxruntime which is not available on Python <3.11")
        with patch('upsonic.embeddings.fastembed_provider.TextEmbedding'):
            provider = create_embedding_provider("fastembed", model_name="BAAI/bge-small-en-v1.5")
            
            assert isinstance(provider, FastEmbedProvider)
            assert provider.config.model_name == "BAAI/bge-small-en-v1.5"
    
    def test_create_embedding_provider_ollama(self):
        """Test creating Ollama embedding provider via factory."""
        with patch('aiohttp.ClientSession'):
            provider = create_embedding_provider("ollama", model_name="nomic-embed-text")
            
            assert isinstance(provider, OllamaEmbedding)
            assert provider.config.model_name == "nomic-embed-text"
    
    def test_create_embedding_provider_gemini(self):
        """Test creating Gemini embedding provider via factory."""
        with patch.dict(os.environ, {'GOOGLE_API_KEY': 'test-key'}):
            with patch('google.genai.Client'):
                provider = create_embedding_provider("gemini", model_name="gemini-embedding-001")
                
                assert isinstance(provider, GeminiEmbedding)
                assert provider.config.model_name == "gemini-embedding-001"
    
    def test_create_embedding_provider_invalid(self):
        """Test creating invalid embedding provider."""
        with pytest.raises(ConfigurationError):
            create_embedding_provider("invalid_provider")


class TestEmbeddingProviderErrorHandling:
    """Test suite for error handling in embedding providers."""
    
    def test_openai_missing_api_key(self):
        """Test OpenAI provider with missing API key."""
        with patch.dict(os.environ, {}, clear=True):
            with pytest.raises(ConfigurationError) as exc_info:
                OpenAIEmbedding()
            
            assert "API key not found" in str(exc_info.value)
    
    def test_azure_openai_missing_endpoint(self):
        """Test Azure OpenAI provider with missing endpoint."""
        with patch.dict(os.environ, {}, clear=True):
            with pytest.raises(ConfigurationError) as exc_info:
                AzureOpenAIEmbedding()

            assert "azure_endpoint" in str(exc_info.value).lower()
    
    def test_bedrock_missing_credentials(self):
        """Test Bedrock provider with missing credentials."""
        with patch('boto3.Session') as mock_session:
            mock_session.side_effect = Exception("No credentials found")
            
            with pytest.raises(ConfigurationError) as exc_info:
                BedrockEmbedding()
            
            assert "no credentials found" in str(exc_info.value).lower()
    
    def test_gemini_missing_api_key(self):
        """Test Gemini provider with missing API key."""
        with patch.dict(os.environ, {}, clear=True):
            with pytest.raises(ConfigurationError) as exc_info:
                GeminiEmbedding()
            
            assert "API key not found" in str(exc_info.value)
    
    @pytest.mark.asyncio
    async def test_embedding_connection_failure(self):
        """Test embedding provider connection failure."""
        provider = MockEmbeddingProvider()
        try:
            # Mock connection failure
            with patch.object(provider, '_embed_batch', side_effect=ModelConnectionError("Connection failed")):
                with pytest.raises(ModelConnectionError):
                    await provider.embed_texts(["test"])
        finally:
            if hasattr(provider, 'close'):
                await provider.close()
    
    @pytest.mark.asyncio
    async def test_embedding_rate_limit_handling(self):
        """Test embedding provider rate limit handling."""
        provider = MockEmbeddingProvider()
        try:
            # Mock rate limit error
            with patch.object(provider, '_embed_batch', side_effect=ModelConnectionError("Rate limit exceeded")):
                with pytest.raises(ModelConnectionError):
                    await provider.embed_texts(["test"])
        finally:
            if hasattr(provider, 'close'):
                await provider.close()


class TestEmbeddingProviderPerformance:
    """Test suite for embedding provider performance."""
    
    @pytest.mark.asyncio
    async def test_embedding_batch_performance(self):
        """Test embedding batch processing performance."""
        provider = MockEmbeddingProvider()
        try:
            # Test with various batch sizes
            batch_sizes = [1, 10, 50, 100]
            
            for batch_size in batch_sizes:
                texts = [f"Test text {i}" for i in range(batch_size)]
                
                start_time = time.time()
                embeddings = await provider.embed_texts(texts)
                end_time = time.time()
                
                assert len(embeddings) == batch_size
                assert all(len(emb) == 384 for emb in embeddings)
                
                # Performance should be reasonable (less than 1 second for mock)
                assert (end_time - start_time) < 1.0
        finally:
            if hasattr(provider, 'close'):
                await provider.close()
    
    @pytest.mark.asyncio
    async def test_embedding_memory_usage(self):
        """Test embedding memory usage."""
        provider = MockEmbeddingProvider()
        try:
            # Test with large batch
            large_texts = [f"Large text content {i}" * 100 for i in range(100)]
            embeddings = await provider.embed_texts(large_texts)
            
            assert len(embeddings) == 100
            assert all(len(emb) == 384 for emb in embeddings)
            
            # Check that memory usage is reasonable
            import sys
            memory_usage = sys.getsizeof(embeddings)
            assert memory_usage < 10 * 1024 * 1024  # Less than 10MB
        finally:
            if hasattr(provider, 'close'):
                await provider.close()
    
    @pytest.mark.asyncio
    async def test_embedding_metrics_collection(self):
        """Test embedding metrics collection."""
        provider = MockEmbeddingProvider()
        try:
            # Initial metrics
            initial_metrics = provider.get_metrics()
            assert initial_metrics.total_chunks == 0
            assert initial_metrics.embedding_time_ms == 0
            
            # After embedding operation
            await provider.embed_texts(["test1", "test2", "test3"])
            
            final_metrics = provider.get_metrics()
            assert final_metrics.total_chunks == 3
            assert final_metrics.embedding_time_ms > 0
        finally:
            if hasattr(provider, 'close'):
                await provider.close()


class TestEmbeddingProviderIntegration:
    """Test suite for embedding provider integration scenarios."""
    
    @pytest.mark.asyncio
    async def test_multiple_embedding_providers(self):
        """Test using multiple embedding providers."""
        providers = [
            MockEmbeddingProvider(config=EmbeddingConfig(model_name="provider1")),
            MockEmbeddingProvider(config=EmbeddingConfig(model_name="provider2")),
            MockEmbeddingProvider(config=EmbeddingConfig(model_name="provider3"))
        ]
        try:
            texts = ["Test text 1", "Test text 2", "Test text 3"]
            
            for provider in providers:
                embeddings = await provider.embed_texts(texts)
                assert len(embeddings) == 3
                assert all(len(emb) == 384 for emb in embeddings)
        finally:
            for provider in providers:
                if hasattr(provider, 'close'):
                    await provider.close()
    
    @pytest.mark.asyncio
    async def test_embedding_provider_switching(self):
        """Test switching between embedding providers."""
        provider1 = MockEmbeddingProvider(config=EmbeddingConfig(model_name="provider1"))
        provider2 = MockEmbeddingProvider(config=EmbeddingConfig(model_name="provider2"))
        try:
            texts = ["Test text for switching"]
            
            # Use first provider
            embeddings1 = await provider1.embed_texts(texts)
            
            # Switch to second provider
            embeddings2 = await provider2.embed_texts(texts)
            
            assert len(embeddings1) == len(embeddings2)
            assert len(embeddings1[0]) == len(embeddings2[0])
        finally:
            for provider in [provider1, provider2]:
                if hasattr(provider, 'close'):
                    await provider.close()
    
    @pytest.mark.asyncio
    async def test_embedding_provider_with_different_modes(self):
        """Test embedding providers with different modes."""
        provider = MockEmbeddingProvider()
        try:
            texts = ["Test document", "Test query"]
            
            # Test different embedding modes
            document_embeddings = await provider.embed_texts(texts, mode=EmbeddingMode.DOCUMENT)
            query_embeddings = await provider.embed_texts(texts, mode=EmbeddingMode.QUERY)
            symmetric_embeddings = await provider.embed_texts(texts, mode=EmbeddingMode.SYMMETRIC)
            
            assert len(document_embeddings) == len(query_embeddings) == len(symmetric_embeddings)
            assert all(len(emb) == 384 for emb in document_embeddings)
            assert all(len(emb) == 384 for emb in query_embeddings)
            assert all(len(emb) == 384 for emb in symmetric_embeddings)
        finally:
            if hasattr(provider, 'close'):
                await provider.close()


if __name__ == "__main__":
    # Run the tests
    pytest.main([__file__, "-v", "--tb=short"])
