"""End-to-end tests for hybrid search workflows (P2).

Tests complete hybrid search pipeline including indexing, exact/fuzzy/hybrid modes,
and result relevance with real project structure.
"""

import sqlite3
import tempfile
from pathlib import Path

import pytest

from codexlens.config import Config
from codexlens.entities import SearchResult
from codexlens.search.hybrid_search import HybridSearchEngine
from codexlens.storage.dir_index import DirIndexStore

# Check if pytest-benchmark is available
try:
    import pytest_benchmark
    BENCHMARK_AVAILABLE = True
except ImportError:
    BENCHMARK_AVAILABLE = False


class TestHybridSearchBasics:
    """Basic tests for HybridSearchEngine."""

    @pytest.fixture
    def temp_db(self):
        """Create temporary database."""
        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
            db_path = Path(f.name)
        yield db_path
        if db_path.exists():
            db_path.unlink()

    @pytest.fixture
    def index_store(self, temp_db):
        """Create DirIndexStore instance."""
        store = DirIndexStore(temp_db)
        yield store
        store.close()

    def test_engine_initialization(self):
        """Test HybridSearchEngine initializes with default weights."""
        engine = HybridSearchEngine()
        assert engine.weights == HybridSearchEngine.DEFAULT_WEIGHTS
        assert engine.weights["exact"] == 0.3
        assert engine.weights["fuzzy"] == 0.1
        assert engine.weights["vector"] == 0.6

    def test_engine_custom_weights(self):
        """Test HybridSearchEngine accepts custom weights."""
        custom_weights = {"exact": 0.5, "fuzzy": 0.5, "vector": 0.0}
        engine = HybridSearchEngine(weights=custom_weights)
        assert engine.weights == custom_weights

    def test_search_requires_index(self, temp_db):
        """Test search requires initialized index."""
        engine = HybridSearchEngine()
        # Empty database - should handle gracefully
        results = engine.search(temp_db, "test", limit=10)
        # May return empty or raise error - either is acceptable
        assert isinstance(results, list)


class TestHybridSearchWithSampleProject:
    """Tests with sample project structure."""

    @pytest.fixture
    def sample_project_db(self):
        """Create database with sample Python + TypeScript project."""
        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
            db_path = Path(f.name)

        store = DirIndexStore(db_path)
        store.initialize()

        # Sample Python files
        python_files = {
            "src/auth/authentication.py": """
def authenticate_user(username, password):
    '''Authenticate user with credentials'''
    return check_credentials(username, password)

def check_credentials(user, pwd):
    return True
""",
            "src/auth/authorization.py": """
def authorize_user(user_id, resource):
    '''Authorize user access to resource'''
    return check_permissions(user_id, resource)

def check_permissions(uid, res):
    return True
""",
            "src/models/user.py": """
class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email

    def authenticate(self, password):
        return authenticate_user(self.username, password)
""",
            "src/api/user_api.py": """
from flask import Flask, request

def get_user_by_id(user_id):
    '''Get user by ID'''
    return User.query.get(user_id)

def create_user(username, email):
    '''Create new user'''
    return User(username, email)
""",
        }

        # Sample TypeScript files
        typescript_files = {
            "frontend/auth/AuthService.ts": """
export class AuthService {
    authenticateUser(username: string, password: string): boolean {
        return this.checkCredentials(username, password);
    }

    private checkCredentials(user: string, pwd: string): boolean {
        return true;
    }
}
""",
            "frontend/models/User.ts": """
export interface User {
    id: number;
    username: string;
    email: string;
}

export class UserModel {
    constructor(private user: User) {}

    authenticate(password: string): boolean {
        return new AuthService().authenticateUser(this.user.username, password);
    }
}
""",
        }

        # Index all files
        with store._get_connection() as conn:
            for path, content in {**python_files, **typescript_files}.items():
                lang = "python" if path.endswith(".py") else "typescript"
                name = path.split('/')[-1]
                conn.execute(
                    """INSERT INTO files (name, full_path, content, language, mtime)
                       VALUES (?, ?, ?, ?, ?)""",
                    (name, path, content, lang, 0.0)
                )
            conn.commit()

        yield db_path
        store.close()

        if db_path.exists():
            db_path.unlink()

    def test_exact_search_mode(self, sample_project_db):
        """Test exact FTS search mode."""
        engine = HybridSearchEngine()

        # Search for "authenticate"
        results = engine.search(
            sample_project_db,
            "authenticate",
            limit=10,
            enable_fuzzy=False,
            enable_vector=False
        )

        assert len(results) > 0, "Should find matches for 'authenticate'"
        # Check results contain expected files
        paths = [r.path for r in results]
        assert any("authentication.py" in p for p in paths)

    def test_fuzzy_search_mode(self, sample_project_db):
        """Test fuzzy FTS search mode."""
        engine = HybridSearchEngine()

        # Search with typo: "authentcate" (missing 'i')
        results = engine.search(
            sample_project_db,
            "authentcate",
            limit=10,
            enable_fuzzy=True,
            enable_vector=False
        )

        # Fuzzy search should still find matches
        assert isinstance(results, list)
        # May or may not find matches depending on trigram support

    def test_hybrid_search_mode(self, sample_project_db):
        """Test hybrid search combines exact and fuzzy."""
        engine = HybridSearchEngine()

        # Hybrid search
        results = engine.search(
            sample_project_db,
            "authenticate",
            limit=10,
            enable_fuzzy=True,
            enable_vector=False
        )

        assert len(results) > 0, "Hybrid search should find matches"
        # Results should have fusion scores
        for result in results:
            assert result.score > 0, "Results should have fusion scores"

    def test_camelcase_query_expansion(self, sample_project_db):
        """Test CamelCase query expansion improves recall."""
        engine = HybridSearchEngine()

        # Search for "AuthService" (CamelCase)
        results = engine.search(
            sample_project_db,
            "AuthService",
            limit=10,
            enable_fuzzy=False
        )

        # Should find TypeScript AuthService class
        paths = [r.path for r in results]
        assert any("AuthService.ts" in p for p in paths), \
            "Should find AuthService with CamelCase query"

    def test_snake_case_query_expansion(self, sample_project_db):
        """Test snake_case query expansion improves recall."""
        engine = HybridSearchEngine()

        # Search for "get_user_by_id" (snake_case)
        results = engine.search(
            sample_project_db,
            "get_user_by_id",
            limit=10,
            enable_fuzzy=False
        )

        # Should find Python function
        paths = [r.path for r in results]
        assert any("user_api.py" in p for p in paths), \
            "Should find get_user_by_id with snake_case query"

    def test_partial_identifier_match(self, sample_project_db):
        """Test partial identifier matching with query expansion."""
        engine = HybridSearchEngine()

        # Search for just "User" (part of UserModel, User class, etc.)
        results = engine.search(
            sample_project_db,
            "User",
            limit=10,
            enable_fuzzy=False
        )

        assert len(results) > 0, "Should find matches for 'User'"
        # Should find multiple files with User in name
        paths = [r.path for r in results]
        assert len([p for p in paths if "user" in p.lower()]) > 0


class TestHybridSearchRelevance:
    """Tests for result relevance and ranking."""

    @pytest.fixture
    def relevance_db(self):
        """Create database for testing relevance ranking."""
        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
            db_path = Path(f.name)

        store = DirIndexStore(db_path)
        store.initialize()

        # Files with varying relevance to "authentication"
        files = {
            "auth/authentication.py": """
# Primary authentication module
def authenticate_user(username, password):
    '''Main authentication function'''
    pass

def validate_authentication(token):
    pass
""",
            "auth/auth_helpers.py": """
# Helper functions for authentication
def hash_password(password):
    pass

def verify_authentication_token(token):
    pass
""",
            "models/user.py": """
# User model (mentions authentication once)
class User:
    def check_authentication(self):
        pass
""",
            "utils/logging.py": """
# Logging utility (no authentication mention)
def log_message(msg):
    pass
""",
        }

        with store._get_connection() as conn:
            for path, content in files.items():
                name = path.split('/')[-1]
                conn.execute(
                    """INSERT INTO files (name, full_path, content, language, mtime)
                       VALUES (?, ?, ?, ?, ?)""",
                    (name, path, content, "python", 0.0)
                )
            conn.commit()

        yield db_path
        store.close()

        if db_path.exists():
            db_path.unlink()

    def test_exact_match_ranks_higher(self, relevance_db):
        """Test files with exact term matches rank higher."""
        engine = HybridSearchEngine()

        results = engine.search(
            relevance_db,
            "authentication",
            limit=10,
            enable_fuzzy=False
        )

        # First result should be authentication.py (most mentions)
        assert len(results) > 0
        assert "authentication.py" in results[0].path, \
            "File with most mentions should rank first"

    def test_hybrid_fusion_improves_ranking(self, relevance_db):
        """Test hybrid RRF fusion improves ranking over single source."""
        engine = HybridSearchEngine()

        # Exact only
        exact_results = engine.search(
            relevance_db,
            "authentication",
            limit=5,
            enable_fuzzy=False
        )

        # Hybrid
        hybrid_results = engine.search(
            relevance_db,
            "authentication",
            limit=5,
            enable_fuzzy=True
        )

        # Both should find matches
        assert len(exact_results) > 0
        assert len(hybrid_results) > 0

        # Hybrid may rerank results
        assert isinstance(hybrid_results[0], SearchResult)


class TestHybridSearchPerformance:
    """Performance tests for hybrid search."""

    @pytest.fixture
    def large_project_db(self):
        """Create database with many files."""
        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
            db_path = Path(f.name)

        store = DirIndexStore(db_path)
        store.initialize()

        # Create 100 test files
        with store._get_connection() as conn:
            for i in range(100):
                content = f"""
def function_{i}(param):
    '''Test function {i}'''
    return authenticate_user(param)

class Class{i}:
    def method_{i}(self):
        pass
"""
                path = f"src/module_{i}.py"
                name = f"module_{i}.py"
                conn.execute(
                    """INSERT INTO files (name, full_path, content, language, mtime)
                       VALUES (?, ?, ?, ?, ?)""",
                    (name, path, content, "python", 0.0)
                )
            conn.commit()

        yield db_path
        store.close()

        if db_path.exists():
            db_path.unlink()

    @pytest.mark.skipif(not BENCHMARK_AVAILABLE, reason="pytest-benchmark not installed")
    def test_search_latency(self, large_project_db, benchmark):
        """Benchmark search latency."""
        engine = HybridSearchEngine()

        def search_query():
            return engine.search(
                large_project_db,
                "authenticate",
                limit=20,
                enable_fuzzy=True
            )

        # Should complete in reasonable time
        results = benchmark(search_query)
        assert isinstance(results, list)

    def test_hybrid_overhead(self, large_project_db):
        """Test hybrid search overhead vs exact search."""
        engine = HybridSearchEngine()

        import time

        # Measure exact search time
        start = time.time()
        exact_results = engine.search(
            large_project_db,
            "authenticate",
            limit=20,
            enable_fuzzy=False
        )
        exact_time = time.time() - start

        # Measure hybrid search time
        start = time.time()
        hybrid_results = engine.search(
            large_project_db,
            "authenticate",
            limit=20,
            enable_fuzzy=True
        )
        hybrid_time = time.time() - start

        # Hybrid should be <10x slower than exact (relaxed for CI stability and ANN initialization overhead)
        if exact_time > 0:
            overhead = hybrid_time / exact_time
            assert overhead < 10.0, f"Hybrid overhead {overhead:.1f}x should be <10x"


class TestHybridSearchEdgeCases:
    """Edge case tests for hybrid search."""

    @pytest.fixture
    def temp_db(self):
        """Create temporary database."""
        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
            db_path = Path(f.name)

        # Initialize with schema
        DirIndexStore(db_path)

        yield db_path
        # Ignore file deletion errors on Windows (SQLite file lock)
        try:
            if db_path.exists():
                db_path.unlink()
        except PermissionError:
            pass

    def test_empty_index_search(self, temp_db):
        """Test search on empty index returns empty results."""
        engine = HybridSearchEngine()

        results = engine.search(temp_db, "test", limit=10)
        assert results == [] or isinstance(results, list)

    def test_no_matches_query(self, temp_db):
        """Test query with no matches returns empty results."""
        store = DirIndexStore(temp_db)
        store.initialize()

        try:
            # Index one file
            with store._get_connection() as conn:
                conn.execute(
                    """INSERT INTO files (name, full_path, content, language, mtime)
                       VALUES (?, ?, ?, ?, ?)""",
                    ("test.py", "test.py", "def hello(): pass", "python", 0.0)
                )
                conn.commit()

            engine = HybridSearchEngine()
            results = engine.search(temp_db, "nonexistent", limit=10)

            assert results == [] or len(results) == 0
        finally:
            store.close()

    def test_special_characters_in_query(self, temp_db):
        """Test queries with special characters are handled."""
        store = DirIndexStore(temp_db)
        store.initialize()

        try:
            # Index file
            with store._get_connection() as conn:
                conn.execute(
                    """INSERT INTO files (name, full_path, content, language, mtime)
                       VALUES (?, ?, ?, ?, ?)""",
                    ("test.py", "test.py", "def test(): pass", "python", 0.0)
                )
                conn.commit()

            engine = HybridSearchEngine()

            # Query with special chars should not crash
            queries = ["test*", "test?", "test&", "test|"]
            for query in queries:
                try:
                    results = engine.search(temp_db, query, limit=10)
                    assert isinstance(results, list)
                except Exception:
                    # Some queries may be invalid FTS5 syntax - that's OK
                    pass
        finally:
            store.close()

    def test_very_long_query(self, temp_db):
        """Test very long queries are handled."""
        store = DirIndexStore(temp_db)
        store.initialize()

        try:
            # Index file
            with store._get_connection() as conn:
                conn.execute(
                    """INSERT INTO files (name, full_path, content, language, mtime)
                       VALUES (?, ?, ?, ?, ?)""",
                    ("test.py", "test.py", "def test(): pass", "python", 0.0)
                )
                conn.commit()

            engine = HybridSearchEngine()

            # Very long query
            long_query = "test " * 100
            results = engine.search(temp_db, long_query, limit=10)
            assert isinstance(results, list)
        finally:
            store.close()

    def test_unicode_query(self, temp_db):
        """Test Unicode queries are handled."""
        store = DirIndexStore(temp_db)
        store.initialize()

        try:
            # Index file with Unicode content
            with store._get_connection() as conn:
                conn.execute(
                    """INSERT INTO files (name, full_path, content, language, mtime)
                       VALUES (?, ?, ?, ?, ?)""",
                    ("test.py", "test.py", "def 测试函数(): pass", "python", 0.0)
                )
                conn.commit()

            engine = HybridSearchEngine()

            # Unicode query
            results = engine.search(temp_db, "测试", limit=10)
            assert isinstance(results, list)
        finally:
            store.close()


class TestHybridSearchIntegration:
    """Integration tests for complete workflow."""

    @pytest.fixture
    def project_db(self):
        """Create realistic project database."""
        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
            db_path = Path(f.name)

        store = DirIndexStore(db_path)
        store.initialize()

        # Realistic project structure
        files = {
            "src/authentication/login.py": "def login_user(username, password): pass",
            "src/authentication/logout.py": "def logout_user(session_id): pass",
            "src/authorization/permissions.py": "def check_permission(user, resource): pass",
            "src/models/user_model.py": "class UserModel: pass",
            "src/api/auth_api.py": "def authenticate_api(token): pass",
            "tests/test_auth.py": "def test_authentication(): pass",
        }

        with store._get_connection() as conn:
            for path, content in files.items():
                name = path.split('/')[-1]
                conn.execute(
                    """INSERT INTO files (name, full_path, content, language, mtime)
                       VALUES (?, ?, ?, ?, ?)""",
                    (name, path, content, "python", 0.0)
                )
            conn.commit()

        yield db_path
        store.close()

        if db_path.exists():
            db_path.unlink()

    def test_workflow_index_search_refine(self, project_db):
        """Test complete workflow: index → search → refine."""
        engine = HybridSearchEngine()

        # Initial broad search
        results = engine.search(project_db, "auth", limit=20)
        assert len(results) > 0

        # Refined search
        refined = engine.search(project_db, "authentication", limit=10)
        assert len(refined) > 0

        # Most refined search
        specific = engine.search(project_db, "login_user", limit=5)
        # May or may not find exact match depending on query expansion

    def test_consistency_across_searches(self, project_db):
        """Test search results are consistent across multiple calls."""
        engine = HybridSearchEngine()

        # Same query multiple times
        results1 = engine.search(project_db, "authenticate", limit=10)
        results2 = engine.search(project_db, "authenticate", limit=10)

        # Should return same results (same order)
        assert len(results1) == len(results2)
        if len(results1) > 0:
            assert results1[0].path == results2[0].path


@pytest.mark.integration
class TestHybridSearchFullCoverage:
    """Full coverage integration tests."""

    def test_all_modes_with_real_project(self):
        """Test all search modes (exact, fuzzy, hybrid) with realistic project."""
        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
            db_path = Path(f.name)

        store = None
        try:
            store = DirIndexStore(db_path)
            store.initialize()

            # Create comprehensive test project
            files = {
                "auth.py": "def authenticate(): pass",
                "authz.py": "def authorize(): pass",
                "user.py": "class User: pass",
            }

            with store._get_connection() as conn:
                for path, content in files.items():
                    name = path.split('/')[-1]
                    conn.execute(
                        """INSERT INTO files (name, full_path, content, language, mtime)
                           VALUES (?, ?, ?, ?, ?)""",
                        (name, path, content, "python", 0.0)
                    )
                conn.commit()

            engine = HybridSearchEngine()

            # Test exact mode
            exact = engine.search(db_path, "authenticate", enable_fuzzy=False)
            assert isinstance(exact, list)

            # Test fuzzy mode
            fuzzy = engine.search(db_path, "authenticate", enable_fuzzy=True)
            assert isinstance(fuzzy, list)

            # Test hybrid mode (default)
            hybrid = engine.search(db_path, "authenticate")
            assert isinstance(hybrid, list)

        finally:
            if store:
                store.close()
            if db_path.exists():
                db_path.unlink()



class TestHybridSearchWithVectorMock:
    """Tests for hybrid search with mocked vector search."""
    
    @pytest.fixture
    def mock_vector_db(self):
        """Create database with vector search mocked."""
        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
            db_path = Path(f.name)
        
        store = DirIndexStore(db_path)
        store.initialize()
        
        # Index sample files
        files = {
            "auth/login.py": "def login_user(username, password): authenticate()",
            "auth/logout.py": "def logout_user(session): cleanup_session()",
            "user/profile.py": "class UserProfile: def get_data(): pass"
        }
        
        with store._get_connection() as conn:
            for path, content in files.items():
                name = path.split('/')[-1]
                conn.execute(
                    """INSERT INTO files (name, full_path, content, language, mtime)
                       VALUES (?, ?, ?, ?, ?)""",
                    (name, path, content, "python", 0.0)
                )
            conn.commit()
        
        yield db_path
        store.close()
        
        if db_path.exists():
            db_path.unlink()
    
    def test_hybrid_with_vector_enabled(self, mock_vector_db):
        """Test hybrid search with vector search enabled (mocked)."""
        from unittest.mock import patch, MagicMock
        
        # Mock the vector search to return fake results
        mock_vector_results = [
            SearchResult(path="auth/login.py", score=0.95, content_snippet="login"),
            SearchResult(path="user/profile.py", score=0.75, content_snippet="profile")
        ]
        
        engine = HybridSearchEngine()
        
        # Mock vector search method if it exists
        with patch.object(engine, '_search_vector', return_value=mock_vector_results) if hasattr(engine, '_search_vector') else patch('codexlens.search.hybrid_search.vector_search', return_value=mock_vector_results):
            results = engine.search(
                mock_vector_db,
                "login",
                limit=10,
                enable_fuzzy=True,
                enable_vector=True  # ENABLE vector search
            )
            
            # Should get results from RRF fusion of exact + fuzzy + vector
            assert isinstance(results, list)
            assert len(results) > 0, "Hybrid search with vector should return results"
            
            # Results should have fusion scores
            for result in results:
                assert hasattr(result, 'score')
                assert result.score > 0  # RRF fusion scores are positive


class TestHybridSearchAdaptiveWeights:
    """Integration tests for adaptive RRF weights + reranking gating."""

    def test_adaptive_weights_code_query(self):
        """Exact weight should dominate for code-like queries."""
        from unittest.mock import patch

        engine = HybridSearchEngine()

        results_map = {
            "exact": [SearchResult(path="a.py", score=10.0, excerpt="a")],
            "fuzzy": [SearchResult(path="b.py", score=9.0, excerpt="b")],
            "vector": [SearchResult(path="c.py", score=0.9, excerpt="c")],
        }

        captured = {}
        from codexlens.search import ranking as ranking_module

        def capture_rrf(map_in, weights_in, k=60):
            captured["weights"] = dict(weights_in)
            return ranking_module.reciprocal_rank_fusion(map_in, weights_in, k=k)

        with patch.object(HybridSearchEngine, "_search_parallel", return_value=results_map), patch(
            "codexlens.search.hybrid_search.reciprocal_rank_fusion",
            side_effect=capture_rrf,
        ):
            engine.search(Path("dummy.db"), "def authenticate", enable_vector=True)

        assert captured["weights"]["exact"] > 0.4

    def test_adaptive_weights_nl_query(self):
        """Vector weight should dominate for natural-language queries."""
        from unittest.mock import patch

        engine = HybridSearchEngine()

        results_map = {
            "exact": [SearchResult(path="a.py", score=10.0, excerpt="a")],
            "fuzzy": [SearchResult(path="b.py", score=9.0, excerpt="b")],
            "vector": [SearchResult(path="c.py", score=0.9, excerpt="c")],
        }

        captured = {}
        from codexlens.search import ranking as ranking_module

        def capture_rrf(map_in, weights_in, k=60):
            captured["weights"] = dict(weights_in)
            return ranking_module.reciprocal_rank_fusion(map_in, weights_in, k=k)

        with patch.object(HybridSearchEngine, "_search_parallel", return_value=results_map), patch(
            "codexlens.search.hybrid_search.reciprocal_rank_fusion",
            side_effect=capture_rrf,
        ):
            engine.search(Path("dummy.db"), "how to handle user login", enable_vector=True)

        assert captured["weights"]["vector"] > 0.6

    def test_default_engine_weights_keep_lsp_graph_backend_available(self):
        """Legacy public defaults should not discard LSP graph fusion weights internally."""
        from unittest.mock import patch

        engine = HybridSearchEngine()

        results_map = {
            "exact": [SearchResult(path="a.py", score=10.0, excerpt="a")],
            "fuzzy": [SearchResult(path="b.py", score=9.0, excerpt="b")],
            "vector": [SearchResult(path="c.py", score=0.9, excerpt="c")],
            "lsp_graph": [SearchResult(path="d.py", score=0.8, excerpt="d")],
        }

        captured = {}
        from codexlens.search import ranking as ranking_module

        def capture_rrf(map_in, weights_in, k=60):
            captured["weights"] = dict(weights_in)
            return ranking_module.reciprocal_rank_fusion(map_in, weights_in, k=k)

        with patch.object(HybridSearchEngine, "_search_parallel", return_value=results_map), patch(
            "codexlens.search.hybrid_search.reciprocal_rank_fusion",
            side_effect=capture_rrf,
        ):
            engine.search(Path("dummy.db"), "auth flow", enable_vector=True, enable_lsp_graph=True)

        assert engine.weights == HybridSearchEngine.DEFAULT_WEIGHTS
        assert "lsp_graph" in captured["weights"]
        assert captured["weights"]["lsp_graph"] > 0.0

    def test_reranking_enabled(self, tmp_path):
        """Reranking runs only when explicitly enabled via config."""
        from unittest.mock import patch

        results_map = {
            "exact": [SearchResult(path="a.py", score=10.0, excerpt="a")],
            "fuzzy": [SearchResult(path="b.py", score=9.0, excerpt="b")],
            "vector": [SearchResult(path="c.py", score=0.9, excerpt="c")],
        }

        class DummyEmbedder:
            def embed(self, texts):
                if isinstance(texts, str):
                    texts = [texts]
                return [[1.0, 0.0] for _ in texts]

        # Disabled: should not invoke rerank_results
        config_off = Config(data_dir=tmp_path / "off", enable_reranking=False)
        engine_off = HybridSearchEngine(config=config_off, embedder=DummyEmbedder())

        with patch.object(HybridSearchEngine, "_search_parallel", return_value=results_map), patch(
            "codexlens.search.hybrid_search.rerank_results"
        ) as rerank_mock:
            engine_off.search(Path("dummy.db"), "query", enable_vector=True)
            rerank_mock.assert_not_called()

        # Enabled: should invoke rerank_results once
        config_on = Config(data_dir=tmp_path / "on", enable_reranking=True, reranking_top_k=10)
        engine_on = HybridSearchEngine(config=config_on, embedder=DummyEmbedder())

        with patch.object(HybridSearchEngine, "_search_parallel", return_value=results_map), patch(
            "codexlens.search.hybrid_search.rerank_results",
            side_effect=lambda q, r, e, top_k=50: r,
        ) as rerank_mock:
            engine_on.search(Path("dummy.db"), "query", enable_vector=True)
            assert rerank_mock.call_count == 1

    def test_cross_encoder_reranking_enabled(self, tmp_path):
        """Cross-encoder stage runs only when explicitly enabled via config."""
        from unittest.mock import patch

        results_map = {
            "exact": [SearchResult(path="a.py", score=10.0, excerpt="a")],
            "fuzzy": [SearchResult(path="b.py", score=9.0, excerpt="b")],
            "vector": [SearchResult(path="c.py", score=0.9, excerpt="c")],
        }

        class DummyEmbedder:
            def embed(self, texts):
                if isinstance(texts, str):
                    texts = [texts]
                return [[1.0, 0.0] for _ in texts]

        class DummyReranker:
            def score_pairs(self, pairs, batch_size=32):
                return [0.0 for _ in pairs]

        config = Config(
            data_dir=tmp_path / "ce",
            enable_reranking=True,
            enable_cross_encoder_rerank=True,
            reranker_top_k=10,
        )
        engine = HybridSearchEngine(config=config, embedder=DummyEmbedder())

        with patch.object(HybridSearchEngine, "_search_parallel", return_value=results_map), patch(
            "codexlens.search.hybrid_search.rerank_results",
            side_effect=lambda q, r, e, top_k=50: r,
        ) as rerank_mock, patch.object(
            HybridSearchEngine,
            "_get_cross_encoder_reranker",
            return_value=DummyReranker(),
        ) as get_ce_mock, patch(
            "codexlens.search.hybrid_search.cross_encoder_rerank",
            side_effect=lambda q, r, ce, top_k=50: r,
        ) as ce_mock:
            engine.search(Path("dummy.db"), "query", enable_vector=True)
            assert rerank_mock.call_count == 1
            assert get_ce_mock.call_count == 1
            assert ce_mock.call_count == 1
