"""Tests for rbsp.core.embeddings -- Embedding Pipeline Abstraction."""

from __future__ import annotations

import math
import os
import random
import sys
import threading
import time
import unittest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from rbsp.core.chunker import Chunk, DocumentChunker
from rbsp.core.embeddings import (
    DummyProvider,
    EmbeddingConfig,
    EmbeddingPipeline,
    EmbeddingProvider,
    OllamaProvider,
    _l2_normalize,
    create_pipeline,
    create_provider,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _vec_norm(vec):
    """Compute L2 norm of a vector."""
    return math.sqrt(sum(x * x for x in vec))


# ---------------------------------------------------------------------------
# EmbeddingConfig
# ---------------------------------------------------------------------------


class TestEmbeddingConfig(unittest.TestCase):
    """EmbeddingConfig defaults and validation."""

    def test_defaults(self):
        cfg = EmbeddingConfig()
        self.assertEqual(cfg.provider, "dummy")
        self.assertIsNone(cfg.model)
        self.assertIsNone(cfg.dim)
        self.assertEqual(cfg.batch_size, 64)
        self.assertEqual(cfg.chunk_size, 384)
        self.assertEqual(cfg.overlap, 64)
        self.assertEqual(cfg.strategy, "sentence")
        self.assertEqual(cfg.min_chunk_size, 32)
        self.assertIsNone(cfg.api_key)
        self.assertIsNone(cfg.base_url)
        self.assertTrue(cfg.normalize)
        self.assertEqual(cfg.seed, 42)

    def test_custom_values(self):
        cfg = EmbeddingConfig(
            provider="openai",
            model="text-embedding-3-large",
            dim=3072,
            batch_size=32,
            chunk_size=256,
            overlap=32,
            strategy="fixed",
            min_chunk_size=16,
            api_key="sk-test",
            base_url="https://custom.api.com",
            normalize=False,
            seed=99,
        )
        self.assertEqual(cfg.provider, "openai")
        self.assertEqual(cfg.model, "text-embedding-3-large")
        self.assertEqual(cfg.dim, 3072)
        self.assertEqual(cfg.batch_size, 32)
        self.assertEqual(cfg.chunk_size, 256)
        self.assertEqual(cfg.overlap, 32)
        self.assertEqual(cfg.strategy, "fixed")
        self.assertEqual(cfg.min_chunk_size, 16)
        self.assertEqual(cfg.api_key, "sk-test")
        self.assertEqual(cfg.base_url, "https://custom.api.com")
        self.assertFalse(cfg.normalize)
        self.assertEqual(cfg.seed, 99)

    def test_invalid_provider_raises(self):
        with self.assertRaises(ValueError):
            EmbeddingConfig(provider="unknown")

    def test_invalid_batch_size_raises(self):
        with self.assertRaises(ValueError):
            EmbeddingConfig(batch_size=0)

    def test_invalid_chunk_size_raises(self):
        with self.assertRaises(ValueError):
            EmbeddingConfig(chunk_size=0)

    def test_negative_overlap_raises(self):
        with self.assertRaises(ValueError):
            EmbeddingConfig(overlap=-1)

    def test_overlap_ge_chunk_size_raises(self):
        with self.assertRaises(ValueError):
            EmbeddingConfig(chunk_size=100, overlap=100)

    def test_invalid_strategy_raises(self):
        with self.assertRaises(ValueError):
            EmbeddingConfig(strategy="invalid")

    def test_invalid_min_chunk_size_raises(self):
        with self.assertRaises(ValueError):
            EmbeddingConfig(min_chunk_size=0)

    def test_repr(self):
        cfg = EmbeddingConfig()
        r = repr(cfg)
        self.assertIn("EmbeddingConfig", r)
        self.assertIn("dummy", r)
        self.assertIn("chunk_size=384", r)
        self.assertIn("sentence", r)

    def test_valid_providers(self):
        for p in ("dummy", "openai", "cohere", "ollama"):
            cfg = EmbeddingConfig(provider=p)
            self.assertEqual(cfg.provider, p)

    def test_valid_strategies(self):
        for s in ("fixed", "sentence", "paragraph"):
            cfg = EmbeddingConfig(strategy=s)
            self.assertEqual(cfg.strategy, s)


# ---------------------------------------------------------------------------
# DummyProvider
# ---------------------------------------------------------------------------


class TestDummyProvider(unittest.TestCase):
    """DummyProvider deterministic embedding generation."""

    def test_embed_returns_correct_dim(self):
        provider = DummyProvider(dim=64)
        vec = provider.embed("hello world")
        self.assertEqual(len(vec), 64)

    def test_embed_default_dim(self):
        provider = DummyProvider()
        vec = provider.embed("test")
        self.assertEqual(len(vec), 128)

    def test_dim_property(self):
        provider = DummyProvider(dim=256)
        self.assertEqual(provider.dim, 256)

    def test_deterministic(self):
        """Same text + same seed produces identical vectors."""
        p1 = DummyProvider(dim=32, seed=42)
        p2 = DummyProvider(dim=32, seed=42)
        v1 = p1.embed("hello")
        v2 = p2.embed("hello")
        self.assertEqual(v1, v2)

    def test_different_text_different_vectors(self):
        provider = DummyProvider(dim=32)
        v1 = provider.embed("hello")
        v2 = provider.embed("world")
        self.assertNotEqual(v1, v2)

    def test_different_seed_different_vectors(self):
        p1 = DummyProvider(dim=32, seed=1)
        p2 = DummyProvider(dim=32, seed=2)
        v1 = p1.embed("hello")
        v2 = p2.embed("hello")
        self.assertNotEqual(v1, v2)

    def test_normalized_output(self):
        """When normalize=True (default), vectors should be unit length."""
        provider = DummyProvider(dim=64)
        vec = provider.embed("test normalization")
        norm = _vec_norm(vec)
        self.assertAlmostEqual(norm, 1.0, places=5)

    def test_unnormalized_output(self):
        """When normalize=False, vectors should NOT be unit length in general."""
        provider = DummyProvider(dim=64, normalize=False)
        vec = provider.embed("test no normalization")
        norm = _vec_norm(vec)
        self.assertNotAlmostEqual(norm, 1.0, places=3)

    def test_embed_batch(self):
        provider = DummyProvider(dim=32)
        texts = ["hello", "world", "foo"]
        batch_results = provider.embed_batch(texts)
        self.assertEqual(len(batch_results), 3)
        for text, batch_vec in zip(texts, batch_results):
            single_vec = provider.embed(text)
            self.assertEqual(batch_vec, single_vec)

    def test_embed_batch_empty(self):
        provider = DummyProvider(dim=32)
        result = provider.embed_batch([])
        self.assertEqual(result, [])

    def test_embed_empty_string(self):
        provider = DummyProvider(dim=32)
        vec = provider.embed("")
        self.assertEqual(len(vec), 32)
        norm = _vec_norm(vec)
        self.assertAlmostEqual(norm, 1.0, places=5)

    def test_repr(self):
        provider = DummyProvider(dim=64, seed=99)
        r = repr(provider)
        self.assertIn("DummyProvider", r)
        self.assertIn("dim=64", r)
        self.assertIn("seed=99", r)

    def test_high_dimensional(self):
        """1024-dimensional vectors should work."""
        provider = DummyProvider(dim=1024)
        vec = provider.embed("high dim test")
        self.assertEqual(len(vec), 1024)
        norm = _vec_norm(vec)
        self.assertAlmostEqual(norm, 1.0, places=5)

    def test_dim_one(self):
        """Edge case: single-dimensional vector."""
        provider = DummyProvider(dim=1)
        vec = provider.embed("dim one")
        self.assertEqual(len(vec), 1)


# ---------------------------------------------------------------------------
# EmbeddingProvider ABC
# ---------------------------------------------------------------------------


class TestEmbeddingProviderABC(unittest.TestCase):
    """EmbeddingProvider cannot be instantiated directly."""

    def test_cannot_instantiate(self):
        with self.assertRaises(TypeError):
            EmbeddingProvider()

    def test_subclass_must_implement_embed(self):
        class Incomplete(EmbeddingProvider):
            @property
            def dim(self) -> int:
                return 10

        with self.assertRaises(TypeError):
            Incomplete()

    def test_subclass_must_implement_dim(self):
        class Incomplete(EmbeddingProvider):
            def embed(self, text: str):
                return [0.0]

        with self.assertRaises(TypeError):
            Incomplete()

    def test_complete_subclass_works(self):
        class Complete(EmbeddingProvider):
            def embed(self, text: str):
                return [0.0, 1.0]

            @property
            def dim(self) -> int:
                return 2

        p = Complete()
        self.assertEqual(p.dim, 2)
        self.assertEqual(p.embed("test"), [0.0, 1.0])
        results = p.embed_batch(["a", "b"])
        self.assertEqual(len(results), 2)


# ---------------------------------------------------------------------------
# EmbeddingPipeline
# ---------------------------------------------------------------------------


class TestEmbeddingPipeline(unittest.TestCase):
    """EmbeddingPipeline combines chunker + provider."""

    def setUp(self):
        self.provider = DummyProvider(dim=32, seed=42)
        self.chunker = DocumentChunker(chunk_size=10, overlap=2, strategy="fixed")
        self.pipeline = EmbeddingPipeline(
            provider=self.provider, chunker=self.chunker
        )

    def test_dim_property(self):
        self.assertEqual(self.pipeline.dim, 32)

    def test_provider_property(self):
        self.assertIs(self.pipeline.provider, self.provider)

    def test_embed_text(self):
        vec = self.pipeline.embed_text("hello world")
        self.assertEqual(len(vec), 32)
        norm = _vec_norm(vec)
        self.assertAlmostEqual(norm, 1.0, places=5)

    def test_embed_texts(self):
        vecs = self.pipeline.embed_texts(["hello", "world"])
        self.assertEqual(len(vecs), 2)
        self.assertEqual(len(vecs[0]), 32)
        self.assertEqual(len(vecs[1]), 32)

    def test_vectorize_document_short(self):
        """Short document fits in a single chunk."""
        results = self.pipeline.vectorize_document("hello world")
        self.assertEqual(len(results), 1)
        chunk, vec = results[0]
        self.assertIsInstance(chunk, Chunk)
        self.assertEqual(len(vec), 32)

    def test_vectorize_document_long(self):
        """Long document gets split into multiple chunks."""
        words = [f"word{i}" for i in range(50)]
        text = " ".join(words)
        results = self.pipeline.vectorize_document(text)
        self.assertGreater(len(results), 1)
        for chunk, vec in results:
            self.assertIsInstance(chunk, Chunk)
            self.assertEqual(len(vec), 32)

    def test_vectorize_returns_chunk_metadata(self):
        """vectorize_document returns Chunk objects with full metadata."""
        words = [f"word{i}" for i in range(25)]
        text = " ".join(words)
        results = self.pipeline.vectorize_document(text, doc_id="test_doc")
        self.assertGreater(len(results), 1)
        for chunk, vec in results:
            self.assertEqual(chunk.doc_id, "test_doc")
            self.assertGreaterEqual(chunk.chunk_index, 0)
            self.assertGreater(chunk.token_count, 0)
            self.assertGreaterEqual(chunk.start_offset, 0)

    def test_default_chunker(self):
        """Pipeline creates a default chunker if none provided."""
        pipeline = EmbeddingPipeline(provider=self.provider)
        vec = pipeline.embed_text("test")
        self.assertEqual(len(vec), 32)

    def test_repr(self):
        r = repr(self.pipeline)
        self.assertIn("EmbeddingPipeline", r)
        self.assertIn("DummyProvider", r)
        self.assertIn("DocumentChunker", r)


# ---------------------------------------------------------------------------
# EmbeddingPipeline -- thread safety
# ---------------------------------------------------------------------------


class TestPipelineThreadSafety(unittest.TestCase):
    """Concurrent embed operations should not corrupt results."""

    def test_concurrent_embeds(self):
        provider = DummyProvider(dim=32, seed=42)
        pipeline = EmbeddingPipeline(provider=provider)
        errors: list = []

        def embedder(start: int, count: int):
            try:
                for i in range(count):
                    vec = pipeline.embed_text(f"text_{start}_{i}")
                    if len(vec) != 32:
                        errors.append(
                            f"Wrong dim: {len(vec)} for text_{start}_{i}"
                        )
            except Exception as exc:
                errors.append(exc)

        threads = []
        for t in range(4):
            th = threading.Thread(target=embedder, args=(t, 25))
            threads.append(th)
            th.start()

        for th in threads:
            th.join()

        self.assertEqual(errors, [], f"Errors: {errors}")

    def test_concurrent_vectorize(self):
        provider = DummyProvider(dim=16, seed=42)
        chunker = DocumentChunker(chunk_size=5, overlap=1, strategy="fixed")
        pipeline = EmbeddingPipeline(provider=provider, chunker=chunker)
        errors: list = []

        def vectorizer():
            try:
                words = [f"w{i}" for i in range(20)]
                text = " ".join(words)
                results = pipeline.vectorize_document(text)
                for chunk, vec in results:
                    if len(vec) != 16:
                        errors.append(f"Wrong dim: {len(vec)}")
            except Exception as exc:
                errors.append(exc)

        threads = [threading.Thread(target=vectorizer) for _ in range(4)]
        for th in threads:
            th.start()
        for th in threads:
            th.join()

        self.assertEqual(errors, [], f"Errors: {errors}")


# ---------------------------------------------------------------------------
# Factory functions
# ---------------------------------------------------------------------------


class TestCreateProvider(unittest.TestCase):
    """create_provider builds providers from config."""

    def test_create_dummy(self):
        cfg = EmbeddingConfig(provider="dummy", dim=64, seed=99)
        p = create_provider(cfg)
        self.assertIsInstance(p, DummyProvider)
        self.assertEqual(p.dim, 64)

    def test_create_dummy_default_dim(self):
        cfg = EmbeddingConfig(provider="dummy")
        p = create_provider(cfg)
        self.assertIsInstance(p, DummyProvider)
        self.assertEqual(p.dim, 128)

    def test_create_openai_missing_dep(self):
        """OpenAI provider should raise ImportError if openai is not installed."""
        cfg = EmbeddingConfig(provider="openai", api_key="test")
        try:
            p = create_provider(cfg)
            from rbsp.core.embeddings import OpenAIProvider
            self.assertIsInstance(p, OpenAIProvider)
        except ImportError:
            pass

    def test_create_cohere_missing_dep(self):
        """Cohere provider should raise ImportError if cohere is not installed."""
        cfg = EmbeddingConfig(provider="cohere", api_key="test")
        try:
            p = create_provider(cfg)
            from rbsp.core.embeddings import CohereProvider
            self.assertIsInstance(p, CohereProvider)
        except ImportError:
            pass

    def test_create_ollama(self):
        cfg = EmbeddingConfig(provider="ollama", model="nomic-embed-text")
        p = create_provider(cfg)
        self.assertIsInstance(p, OllamaProvider)
        self.assertEqual(p.dim, 768)


class TestCreatePipeline(unittest.TestCase):
    """create_pipeline builds a complete pipeline from config."""

    def test_default_pipeline(self):
        pipeline = create_pipeline()
        self.assertIsInstance(pipeline, EmbeddingPipeline)
        self.assertIsInstance(pipeline.provider, DummyProvider)
        self.assertEqual(pipeline.dim, 128)

    def test_custom_pipeline(self):
        cfg = EmbeddingConfig(
            provider="dummy", dim=64, chunk_size=100, overlap=10, seed=99
        )
        pipeline = create_pipeline(cfg)
        self.assertIsInstance(pipeline, EmbeddingPipeline)
        self.assertEqual(pipeline.dim, 64)

    def test_pipeline_embed_works(self):
        pipeline = create_pipeline()
        vec = pipeline.embed_text("test")
        self.assertEqual(len(vec), 128)

    def test_pipeline_vectorize_works(self):
        cfg = EmbeddingConfig(
            provider="dummy", dim=32, chunk_size=5, overlap=1, strategy="fixed"
        )
        pipeline = create_pipeline(cfg)
        words = [f"w{i}" for i in range(20)]
        text = " ".join(words)
        results = pipeline.vectorize_document(text)
        self.assertGreater(len(results), 1)

    def test_pipeline_strategy_propagated(self):
        for s in ("fixed", "sentence", "paragraph"):
            cfg = EmbeddingConfig(
                provider="dummy", dim=16, chunk_size=100, strategy=s
            )
            pipeline = create_pipeline(cfg)
            results = pipeline.vectorize_document("Hello world. Foo bar.")
            self.assertGreater(len(results), 0)


# ---------------------------------------------------------------------------
# OllamaProvider (construction only -- no server needed)
# ---------------------------------------------------------------------------


class TestOllamaProvider(unittest.TestCase):
    """OllamaProvider construction and defaults."""

    def test_default_construction(self):
        p = OllamaProvider()
        self.assertEqual(p.dim, 768)

    def test_custom_model_dim(self):
        p = OllamaProvider(model="all-minilm")
        self.assertEqual(p.dim, 384)

    def test_custom_base_url(self):
        p = OllamaProvider(base_url="http://myhost:1234/")
        self.assertEqual(p._base_url, "http://myhost:1234")

    def test_known_model_dims(self):
        models = {
            "nomic-embed-text": 768,
            "all-minilm": 384,
            "mxbai-embed-large": 1024,
        }
        for model, expected_dim in models.items():
            p = OllamaProvider(model=model)
            self.assertEqual(p.dim, expected_dim, f"Wrong dim for {model}")

    def test_unknown_model_default_dim(self):
        p = OllamaProvider(model="unknown-model")
        self.assertEqual(p.dim, 768)

    def test_explicit_dim_override(self):
        p = OllamaProvider(model="nomic-embed-text", dim=512)
        self.assertEqual(p.dim, 512)

    def test_repr(self):
        p = OllamaProvider(model="nomic-embed-text")
        r = repr(p)
        self.assertIn("OllamaProvider", r)
        self.assertIn("nomic-embed-text", r)


# ---------------------------------------------------------------------------
# _l2_normalize helper
# ---------------------------------------------------------------------------


class TestL2Normalize(unittest.TestCase):
    """Internal _l2_normalize helper."""

    def test_unit_length(self):
        vec = _l2_normalize([3.0, 4.0])
        norm = _vec_norm(vec)
        self.assertAlmostEqual(norm, 1.0, places=10)

    def test_direction_preserved(self):
        vec = _l2_normalize([3.0, 0.0])
        self.assertAlmostEqual(vec[0], 1.0)
        self.assertAlmostEqual(vec[1], 0.0)

    def test_zero_vector_unchanged(self):
        vec = _l2_normalize([0.0, 0.0, 0.0])
        self.assertEqual(vec, [0.0, 0.0, 0.0])

    def test_already_unit(self):
        vec = _l2_normalize([1.0, 0.0, 0.0])
        self.assertAlmostEqual(vec[0], 1.0)
        self.assertAlmostEqual(vec[1], 0.0)
        self.assertAlmostEqual(vec[2], 0.0)

    def test_negative_components(self):
        vec = _l2_normalize([-3.0, -4.0])
        norm = _vec_norm(vec)
        self.assertAlmostEqual(norm, 1.0, places=10)
        self.assertTrue(vec[0] < 0)
        self.assertTrue(vec[1] < 0)


# ---------------------------------------------------------------------------
# Semantic consistency of DummyProvider
# ---------------------------------------------------------------------------


class TestDummyProviderConsistency(unittest.TestCase):
    """Verify DummyProvider produces consistent results across calls."""

    def test_repeated_calls_same_result(self):
        provider = DummyProvider(dim=64, seed=42)
        v1 = provider.embed("consistent text")
        v2 = provider.embed("consistent text")
        self.assertEqual(v1, v2)

    def test_similar_text_different_vectors(self):
        """Even slightly different text should produce different vectors."""
        provider = DummyProvider(dim=64, seed=42)
        v1 = provider.embed("hello world")
        v2 = provider.embed("hello world!")
        self.assertNotEqual(v1, v2)

    def test_vectors_have_reasonable_spread(self):
        """Vectors for different texts should not all be the same."""
        provider = DummyProvider(dim=32, seed=42)
        vectors = [provider.embed(f"text number {i}") for i in range(10)]
        for i in range(len(vectors)):
            for j in range(i + 1, len(vectors)):
                self.assertNotEqual(vectors[i], vectors[j])


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


class TestEdgeCases(unittest.TestCase):
    """Edge cases: unicode, very long text, dimension consistency."""

    def test_unicode_text_embedding(self):
        """Unicode text should embed without errors."""
        provider = DummyProvider(dim=32, seed=42)
        vec = provider.embed("Caf\u00e9 \u00fc\u00f1\u00efc\u00f6d\u00e9")
        self.assertEqual(len(vec), 32)
        norm = _vec_norm(vec)
        self.assertAlmostEqual(norm, 1.0, places=5)

    def test_very_long_text(self):
        """Very long text should chunk and embed without error."""
        rng = random.Random(42)
        words = [f"word{rng.randint(0, 9999)}" for _ in range(10000)]
        text = " ".join(words)
        pipeline = create_pipeline(
            EmbeddingConfig(
                provider="dummy", dim=32, chunk_size=100, overlap=10,
                strategy="fixed",
            )
        )
        results = pipeline.vectorize_document(text)
        self.assertGreater(len(results), 50)
        for chunk, vec in results:
            self.assertEqual(len(vec), 32)

    def test_dimension_consistency_across_calls(self):
        """All embed calls return vectors of the same dimension."""
        provider = DummyProvider(dim=48, seed=42)
        rng = random.Random(42)
        for _ in range(100):
            text = f"text_{rng.randint(0, 999999)}"
            vec = provider.embed(text)
            self.assertEqual(len(vec), 48)


# ---------------------------------------------------------------------------
# Integration tests
# ---------------------------------------------------------------------------


class TestIntegration(unittest.TestCase):
    """Integration: embed -> similarity and chunk -> embed -> store."""

    def test_embed_similarity_roundtrip(self):
        """Identical texts produce identical vectors, different texts differ."""
        provider = DummyProvider(dim=128, seed=42)
        v_a1 = provider.embed("the quick brown fox")
        v_a2 = provider.embed("the quick brown fox")
        v_b = provider.embed("completely unrelated topic about quantum physics")
        self.assertEqual(v_a1, v_a2)
        self.assertNotEqual(v_a1, v_b)

    def test_chunk_embed_store_pipeline(self):
        """Full pipeline: chunk document, embed chunks, verify storage ids."""
        provider = DummyProvider(dim=32, seed=42)
        chunker = DocumentChunker(chunk_size=5, overlap=1, strategy="fixed")
        pipeline = EmbeddingPipeline(provider=provider, chunker=chunker)

        doc_text = " ".join([f"word{i}" for i in range(20)])
        doc_id = "doc_001"

        results = pipeline.vectorize_document(doc_text, doc_id=doc_id)
        self.assertGreater(len(results), 1)

        stored = {}
        for chunk, vector in results:
            store_id = f"{chunk.doc_id}::{chunk.chunk_index}"
            stored[store_id] = (chunk, vector)

        for store_id, (chunk, vector) in stored.items():
            self.assertIn("::", store_id)
            self.assertEqual(len(vector), 32)
            norm = _vec_norm(vector)
            self.assertAlmostEqual(norm, 1.0, places=5)
            self.assertEqual(chunk.doc_id, doc_id)

    def test_batch_embed_matches_single(self):
        """embed_batch results should match individual embed calls."""
        provider = DummyProvider(dim=64, seed=42)
        texts = [f"document number {i}" for i in range(10)]
        batch_vecs = provider.embed_batch(texts)
        single_vecs = [provider.embed(t) for t in texts]
        self.assertEqual(len(batch_vecs), len(single_vecs))
        for bv, sv in zip(batch_vecs, single_vecs):
            self.assertEqual(bv, sv)

    def test_pipeline_config_roundtrip(self):
        """Config -> pipeline -> embed should work end-to-end."""
        cfg = EmbeddingConfig(
            provider="dummy", dim=64, chunk_size=20, overlap=4,
            strategy="fixed", seed=99, normalize=True,
        )
        pipeline = create_pipeline(cfg)
        self.assertEqual(pipeline.dim, 64)
        vec = pipeline.embed_text("hello world")
        self.assertEqual(len(vec), 64)
        norm = _vec_norm(vec)
        self.assertAlmostEqual(norm, 1.0, places=5)


# ---------------------------------------------------------------------------
# Performance
# ---------------------------------------------------------------------------


class TestPerformance(unittest.TestCase):
    """Performance benchmarks for embedding pipeline."""

    def test_1000_embeddings_under_2s(self):
        """1000 embeddings with DummyProvider should complete quickly."""
        provider = DummyProvider(dim=128, seed=42)
        texts = [f"document text number {i} with some content" for i in range(1000)]
        t0 = time.perf_counter()
        for text in texts:
            provider.embed(text)
        elapsed = time.perf_counter() - t0
        self.assertLess(elapsed, 2.0, f"1000 embeddings took {elapsed:.2f}s")

    def test_batch_1000_under_2s(self):
        """Batch embedding 1000 texts should also be fast."""
        provider = DummyProvider(dim=128, seed=42)
        texts = [f"batch document {i} with content" for i in range(1000)]
        t0 = time.perf_counter()
        results = provider.embed_batch(texts)
        elapsed = time.perf_counter() - t0
        self.assertEqual(len(results), 1000)
        self.assertLess(elapsed, 2.0, f"Batch 1000 took {elapsed:.2f}s")


# ---------------------------------------------------------------------------
# Pure-Python fallback paths (zero external dependencies)
# ---------------------------------------------------------------------------


class TestFallbackPaths(unittest.TestCase):
    """Pure-Python fallbacks work without optional dependencies."""

    def test_embed_without_numpy(self):
        """Embedding works with pure-Python path (no numpy)."""
        provider = DummyProvider(dim=8)
        result = provider.embed("hello world")
        self.assertIsInstance(result, list)
        self.assertEqual(len(result), 8)
        for val in result:
            self.assertIsInstance(val, float)

    def test_batch_embed_returns_lists_not_arrays(self):
        """Batch embed returns list-of-lists, not numpy arrays."""
        provider = DummyProvider(dim=4)
        results = provider.embed_batch(["a", "b", "c"])
        self.assertIsInstance(results, list)
        for vec in results:
            self.assertIsInstance(vec, list)
            for val in vec:
                self.assertIsInstance(val, float)

    def test_pipeline_uses_only_stdlib_types(self):
        """Full pipeline returns only stdlib types (list, tuple, float)."""
        pipeline = create_pipeline(
            EmbeddingConfig(provider="dummy", dim=8, chunk_size=5, overlap=1, strategy="fixed")
        )
        results = pipeline.vectorize_document("hello world foo bar baz qux quux")
        self.assertIsInstance(results, list)
        for chunk, vec in results:
            self.assertIsInstance(vec, list)
            for val in vec:
                self.assertIsInstance(val, float)


if __name__ == "__main__":
    unittest.main()
