"""Full coverage tests for CodexLens search functionality.

Comprehensive test suite covering:
- Chain search engine internals
- Multi-directory hierarchical search
- Result merging and deduplication
- Context manager behavior
- Semantic search integration
- Edge cases and error recovery
- Parallel search stress tests
- Boundary conditions
"""

import tempfile
import pytest
import threading
import time
from pathlib import Path
from unittest.mock import MagicMock, patch, PropertyMock
from concurrent.futures import ThreadPoolExecutor

from codexlens.storage.sqlite_store import SQLiteStore
from codexlens.storage.dir_index import DirIndexStore
from codexlens.storage.registry import RegistryStore
from codexlens.storage.path_mapper import PathMapper
from codexlens.search import (
    ChainSearchEngine,
    SearchOptions,
    SearchStats,
    ChainSearchResult,
    quick_search,
)
from codexlens.entities import IndexedFile, Symbol, SearchResult


# === Fixtures ===

@pytest.fixture
def temp_dir():
    """Create a temporary directory."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield Path(tmpdir)


@pytest.fixture
def mock_registry():
    """Create a mock registry."""
    registry = MagicMock(spec=RegistryStore)
    registry.find_nearest_index.return_value = None
    return registry


@pytest.fixture
def mock_mapper():
    """Create a mock path mapper."""
    return MagicMock(spec=PathMapper)


@pytest.fixture
def sample_code_files():
    """Sample code file data for comprehensive testing."""
    return [
        # Authentication module
        {
            "name": "auth.py",
            "language": "python",
            "content": """
def authenticate(username, password):
    '''Authenticate user with credentials.'''
    user = find_user(username)
    if user and check_password(user, password):
        return create_token(user)
    return None

def verify_token(token):
    '''Verify JWT token validity.'''
    try:
        payload = decode_token(token)
        return payload
    except TokenExpired:
        return None

class AuthManager:
    '''Manages authentication state.'''
    def __init__(self):
        self.sessions = {}

    def login(self, user):
        token = authenticate(user.name, user.password)
        self.sessions[user.id] = token
        return token
""",
            "symbols": [
                Symbol(name="authenticate", kind="function", range=(2, 8)),
                Symbol(name="verify_token", kind="function", range=(10, 17)),
                Symbol(name="AuthManager", kind="class", range=(19, 28)),
            ],
        },
        # Database module
        {
            "name": "database.py",
            "language": "python",
            "content": """
def connect(host, port, database):
    '''Establish database connection.'''
    return Connection(host, port, database)

def query(connection, sql, params=None):
    '''Execute SQL query and return results.'''
    cursor = connection.cursor()
    cursor.execute(sql, params or [])
    return cursor.fetchall()

class DatabasePool:
    '''Connection pool for database.'''
    def __init__(self, size=10):
        self.pool = []
        self.size = size

    def get_connection(self):
        if self.pool:
            return self.pool.pop()
        return connect()
""",
            "symbols": [
                Symbol(name="connect", kind="function", range=(2, 4)),
                Symbol(name="query", kind="function", range=(6, 10)),
                Symbol(name="DatabasePool", kind="class", range=(12, 21)),
            ],
        },
        # Utils module
        {
            "name": "utils.py",
            "language": "python",
            "content": """
def format_date(date, fmt='%Y-%m-%d'):
    return date.strftime(fmt)

def parse_json(data):
    '''Parse JSON string to dictionary.'''
    import json
    return json.loads(data)

def hash_password(password, salt=None):
    '''Hash password using bcrypt.'''
    import hashlib
    salt = salt or generate_salt()
    return hashlib.sha256((password + salt).encode()).hexdigest()
""",
            "symbols": [
                Symbol(name="format_date", kind="function", range=(2, 3)),
                Symbol(name="parse_json", kind="function", range=(5, 8)),
                Symbol(name="hash_password", kind="function", range=(10, 14)),
            ],
        },
    ]


@pytest.fixture
def populated_single_store(temp_dir, sample_code_files):
    """Create a single populated DirIndexStore."""
    db_path = temp_dir / "_index.db"
    store = DirIndexStore(db_path)
    store.initialize()

    for file_data in sample_code_files:
        store.add_file(
            name=file_data["name"],
            full_path=str(temp_dir / file_data["name"]),
            content=file_data["content"],
            language=file_data["language"],
            symbols=file_data["symbols"],
        )

    yield store
    store.close()


@pytest.fixture
def hierarchical_index_structure(temp_dir, sample_code_files):
    """Create a multi-level directory index structure for chain search testing.

    Structure:
    project/
        _index.db (root)
        src/
            _index.db
            auth/
                _index.db
            db/
                _index.db
        tests/
            _index.db
    """
    structure = {}

    # Root directory
    root_dir = temp_dir / "project"
    root_dir.mkdir()
    root_db = root_dir / "_index.db"
    root_store = DirIndexStore(root_db)
    root_store.initialize()
    root_store.add_file(
        name="main.py",
        full_path=str(root_dir / "main.py"),
        content="# Main entry point\nfrom src import auth, db\ndef main(): pass",
        language="python",
        symbols=[Symbol(name="main", kind="function", range=(3, 3))],
    )
    structure["root"] = {"path": root_dir, "db": root_db, "store": root_store}

    # src directory
    src_dir = root_dir / "src"
    src_dir.mkdir()
    src_db = src_dir / "_index.db"
    src_store = DirIndexStore(src_db)
    src_store.initialize()
    src_store.add_file(
        name="__init__.py",
        full_path=str(src_dir / "__init__.py"),
        content="# Source package\nfrom .auth import authenticate\nfrom .db import connect",
        language="python",
    )
    structure["src"] = {"path": src_dir, "db": src_db, "store": src_store}

    # src/auth directory
    auth_dir = src_dir / "auth"
    auth_dir.mkdir()
    auth_db = auth_dir / "_index.db"
    auth_store = DirIndexStore(auth_db)
    auth_store.initialize()
    auth_store.add_file(
        name="auth.py",
        full_path=str(auth_dir / "auth.py"),
        content=sample_code_files[0]["content"],
        language="python",
        symbols=sample_code_files[0]["symbols"],
    )
    structure["auth"] = {"path": auth_dir, "db": auth_db, "store": auth_store}

    # src/db directory
    db_dir = src_dir / "db"
    db_dir.mkdir()
    db_db = db_dir / "_index.db"
    db_store = DirIndexStore(db_db)
    db_store.initialize()
    db_store.add_file(
        name="database.py",
        full_path=str(db_dir / "database.py"),
        content=sample_code_files[1]["content"],
        language="python",
        symbols=sample_code_files[1]["symbols"],
    )
    structure["db"] = {"path": db_dir, "db": db_db, "store": db_store}

    # tests directory
    tests_dir = root_dir / "tests"
    tests_dir.mkdir()
    tests_db = tests_dir / "_index.db"
    tests_store = DirIndexStore(tests_db)
    tests_store.initialize()
    tests_store.add_file(
        name="test_auth.py",
        full_path=str(tests_dir / "test_auth.py"),
        content="import pytest\nfrom src.auth import authenticate\ndef test_authenticate(): assert authenticate('user', 'pass')",
        language="python",
        symbols=[Symbol(name="test_authenticate", kind="function", range=(3, 3))],
    )
    structure["tests"] = {"path": tests_dir, "db": tests_db, "store": tests_store}

    # Link subdirectories
    root_store.register_subdir(name="src", index_path=src_db)
    root_store.register_subdir(name="tests", index_path=tests_db)
    src_store.register_subdir(name="auth", index_path=auth_db)
    src_store.register_subdir(name="db", index_path=db_db)

    # Close all stores before yielding to avoid Windows file locking issues
    root_store.close()
    src_store.close()
    auth_store.close()
    db_store.close()
    tests_store.close()

    yield structure


# === Chain Search Engine Internal Tests ===

class TestChainSearchEngineInternals:
    """Tests for ChainSearchEngine internal methods."""

    def test_context_manager_enter_exit(self, mock_registry, mock_mapper):
        """Test context manager protocol."""
        with ChainSearchEngine(mock_registry, mock_mapper) as engine:
            assert engine is not None
            assert isinstance(engine, ChainSearchEngine)
        # Engine should be closed after exit

    def test_close_without_executor(self, mock_registry, mock_mapper):
        """Test close() when executor was never created."""
        engine = ChainSearchEngine(mock_registry, mock_mapper)
        engine.close()  # Should not raise

    def test_close_with_executor(self, mock_registry, mock_mapper, temp_dir):
        """Test close() properly shuts down executor."""
        # Create index
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(
            name="test.py",
            full_path=str(temp_dir / "test.py"),
            content="test content searchable",
            language="python",
        )
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        # Trigger executor creation
        engine.search("test", temp_dir)

        # Close should work
        engine.close()
        assert engine._executor is None

    def test_get_executor_lazy_initialization(self, mock_registry, mock_mapper):
        """Test executor is lazily initialized."""
        engine = ChainSearchEngine(mock_registry, mock_mapper)
        assert engine._executor is None

        executor = engine._get_executor()
        assert executor is not None
        assert engine._executor is executor

        # Second call returns same instance
        assert engine._get_executor() is executor

        engine.close()

    def test_get_executor_custom_workers(self, mock_registry, mock_mapper):
        """Test executor with custom worker count."""
        engine = ChainSearchEngine(mock_registry, mock_mapper, max_workers=4)
        executor = engine._get_executor()
        assert executor is not None
        engine.close()


class TestIndexPathCollection:
    """Tests for _collect_index_paths method."""

    def test_collect_depth_zero(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test collection with depth=0 returns only start index."""
        structure = hierarchical_index_structure
        root_db = structure["root"]["db"]

        mock_mapper.source_to_index_db.return_value = root_db

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        paths = engine._collect_index_paths(root_db, depth=0)

        assert len(paths) == 1
        assert paths[0] == root_db.resolve()
        engine.close()

    def test_collect_depth_one(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test collection with depth=1 returns root + immediate children."""
        structure = hierarchical_index_structure
        root_db = structure["root"]["db"]

        mock_mapper.source_to_index_db.return_value = root_db

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        paths = engine._collect_index_paths(root_db, depth=1)

        # Should include root, src, tests (not auth/db which are depth 2)
        assert len(paths) == 3
        engine.close()

    def test_collect_depth_unlimited(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test collection with depth=-1 returns all indexes."""
        structure = hierarchical_index_structure
        root_db = structure["root"]["db"]

        mock_mapper.source_to_index_db.return_value = root_db

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        paths = engine._collect_index_paths(root_db, depth=-1)

        # Should include all 5: root, src, tests, auth, db
        assert len(paths) == 5
        engine.close()

    def test_collect_avoids_duplicates(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test collection deduplicates paths."""
        structure = hierarchical_index_structure
        root_db = structure["root"]["db"]

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        paths = engine._collect_index_paths(root_db, depth=-1)

        # All paths should be unique
        path_set = set(str(p) for p in paths)
        assert len(path_set) == len(paths)
        engine.close()

    def test_collect_handles_missing_subdir_index(self, mock_registry, mock_mapper, temp_dir):
        """Test collection handles missing subdirectory indexes gracefully."""
        # Create root with reference to non-existent subdir
        root_db = temp_dir / "_index.db"
        store = DirIndexStore(root_db)
        store.initialize()
        store.add_file(
            name="test.py",
            full_path=str(temp_dir / "test.py"),
            content="test",
            language="python",
        )
        # Add reference to non-existent index
        store.register_subdir(name="missing", index_path=temp_dir / "missing" / "_index.db")
        store.close()

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        paths = engine._collect_index_paths(root_db, depth=-1)

        # Should only include root (missing subdir is skipped)
        assert len(paths) == 1
        engine.close()

    def test_collect_skips_ignored_artifact_indexes(self, mock_registry, mock_mapper, temp_dir):
        """Test collection skips dist/build-style artifact subtrees."""
        root_dir = temp_dir / "project"
        root_dir.mkdir()

        root_db = root_dir / "_index.db"
        root_store = DirIndexStore(root_db)
        root_store.initialize()

        src_dir = root_dir / "src"
        src_dir.mkdir()
        src_db = src_dir / "_index.db"
        src_store = DirIndexStore(src_db)
        src_store.initialize()

        dist_dir = root_dir / "dist"
        dist_dir.mkdir()
        dist_db = dist_dir / "_index.db"
        dist_store = DirIndexStore(dist_db)
        dist_store.initialize()

        workflow_dir = root_dir / ".workflow"
        workflow_dir.mkdir()
        workflow_db = workflow_dir / "_index.db"
        workflow_store = DirIndexStore(workflow_db)
        workflow_store.initialize()

        root_store.register_subdir(name="src", index_path=src_db)
        root_store.register_subdir(name="dist", index_path=dist_db)
        root_store.register_subdir(name=".workflow", index_path=workflow_db)

        root_store.close()
        src_store.close()
        dist_store.close()
        workflow_store.close()

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        paths = engine._collect_index_paths(root_db, depth=-1)

        assert {path.relative_to(root_dir).as_posix() for path in paths} == {
            "_index.db",
            "src/_index.db",
        }
        engine.close()


class TestResultMergeAndRank:
    """Tests for _merge_and_rank method."""

    def test_merge_deduplicates_by_path(self, mock_registry, mock_mapper):
        """Test merging deduplicates results by path."""
        engine = ChainSearchEngine(mock_registry, mock_mapper)

        results = [
            SearchResult(path="/test/file.py", score=10.0, excerpt="match 1"),
            SearchResult(path="/test/file.py", score=5.0, excerpt="match 2"),
            SearchResult(path="/test/other.py", score=8.0, excerpt="match 3"),
        ]

        merged = engine._merge_and_rank(results, limit=10)

        assert len(merged) == 2
        # Should keep highest score for duplicate path
        file_result = next(r for r in merged if r.path == "/test/file.py")
        assert file_result.score == 10.0
        engine.close()

    def test_merge_sorts_by_score_descending(self, mock_registry, mock_mapper):
        """Test merged results are sorted by score descending."""
        engine = ChainSearchEngine(mock_registry, mock_mapper)

        results = [
            SearchResult(path="/test/low.py", score=1.0, excerpt=""),
            SearchResult(path="/test/high.py", score=100.0, excerpt=""),
            SearchResult(path="/test/mid.py", score=50.0, excerpt=""),
        ]

        merged = engine._merge_and_rank(results, limit=10)

        assert merged[0].path == "/test/high.py"
        assert merged[1].path == "/test/mid.py"
        assert merged[2].path == "/test/low.py"
        engine.close()

    def test_merge_respects_limit(self, mock_registry, mock_mapper):
        """Test merge respects limit parameter."""
        engine = ChainSearchEngine(mock_registry, mock_mapper)

        results = [
            SearchResult(path=f"/test/file{i}.py", score=float(i), excerpt="")
            for i in range(100)
        ]

        merged = engine._merge_and_rank(results, limit=5)

        assert len(merged) == 5
        # Should be the top 5 by score
        assert merged[0].score == 99.0
        engine.close()

    def test_merge_empty_results(self, mock_registry, mock_mapper):
        """Test merge handles empty results."""
        engine = ChainSearchEngine(mock_registry, mock_mapper)
        merged = engine._merge_and_rank([], limit=10)
        assert merged == []
        engine.close()

    def test_merge_applies_test_file_penalty_for_non_test_query(self, mock_registry, mock_mapper):
        """Non-test queries should lightly demote test files during merge."""
        engine = ChainSearchEngine(mock_registry, mock_mapper)

        results = [
            SearchResult(path="/repo/tests/test_auth.py", score=10.0, excerpt="match 1"),
            SearchResult(path="/repo/src/auth.py", score=9.0, excerpt="match 2"),
        ]

        merged = engine._merge_and_rank(results, limit=10, query="authenticate users")

        assert merged[0].path == "/repo/src/auth.py"
        assert merged[1].metadata["path_penalty_reasons"] == ["test_file"]
        engine.close()

    def test_merge_applies_generated_file_penalty_for_non_artifact_query(self, mock_registry, mock_mapper):
        """Non-artifact queries should lightly demote generated/build results during merge."""
        engine = ChainSearchEngine(mock_registry, mock_mapper)

        results = [
            SearchResult(path="/repo/dist/auth.js", score=10.0, excerpt="match 1"),
            SearchResult(path="/repo/src/auth.ts", score=9.0, excerpt="match 2"),
        ]

        merged = engine._merge_and_rank(results, limit=10, query="authenticate users")

        assert merged[0].path == "/repo/src/auth.ts"
        assert merged[1].metadata["path_penalty_reasons"] == ["generated_artifact"]
        engine.close()


# === Hierarchical Chain Search Tests ===

class TestHierarchicalChainSearch:
    """Tests for searching across directory hierarchies."""

    def test_search_from_root(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test search starting from root finds results in all subdirectories."""
        structure = hierarchical_index_structure
        root_db = structure["root"]["db"]
        root_path = structure["root"]["path"]

        mock_mapper.source_to_index_db.return_value = root_db

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        result = engine.search("authenticate", root_path)

        # Should find authenticate in auth.py and test_auth.py
        assert len(result.results) >= 1
        assert result.stats.dirs_searched == 5  # All directories
        engine.close()

    def test_search_from_subdir(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test search starting from subdirectory."""
        structure = hierarchical_index_structure
        src_db = structure["src"]["db"]
        src_path = structure["src"]["path"]

        mock_mapper.source_to_index_db.return_value = src_db

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        result = engine.search("authenticate", src_path)

        # Should find only in src subtree (src, auth, db)
        assert result.stats.dirs_searched == 3
        engine.close()

    def test_search_with_depth_limit(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test search respects depth limit."""
        structure = hierarchical_index_structure
        root_db = structure["root"]["db"]
        root_path = structure["root"]["path"]

        mock_mapper.source_to_index_db.return_value = root_db

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        options = SearchOptions(depth=1)
        result = engine.search("authenticate", root_path, options)

        # Depth 1: root + immediate children (src, tests) = 3
        assert result.stats.dirs_searched == 3
        engine.close()

    def test_search_aggregates_results(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test search aggregates results from multiple directories."""
        structure = hierarchical_index_structure
        root_db = structure["root"]["db"]
        root_path = structure["root"]["path"]

        mock_mapper.source_to_index_db.return_value = root_db

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        # Search for term that appears in multiple files
        result = engine.search("def", root_path)

        # Should find results from multiple files
        assert len(result.results) >= 3
        engine.close()


# === Search Files Only Tests ===

class TestSearchFilesOnly:
    """Tests for search_files_only method."""

    def test_returns_list_of_strings(self, mock_registry, mock_mapper, temp_dir):
        """Test search_files_only returns list of path strings."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(
            name="test.py",
            full_path=str(temp_dir / "test.py"),
            content="searchable content here",
            language="python",
        )
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        paths = engine.search_files_only("searchable", temp_dir)

        assert isinstance(paths, list)
        assert all(isinstance(p, str) for p in paths)
        engine.close()

    def test_files_only_faster_than_full(self, mock_registry, mock_mapper, temp_dir):
        """Test files_only search is at least as fast as full search."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()

        # Add multiple files
        for i in range(20):
            store.add_file(
                name=f"file{i}.py",
                full_path=str(temp_dir / f"file{i}.py"),
                content=f"searchable content number {i} with more text to index",
                language="python",
            )
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)

        # Time files_only
        start = time.perf_counter()
        for _ in range(10):
            engine.search_files_only("searchable", temp_dir)
        files_only_time = time.perf_counter() - start

        # Time full search
        start = time.perf_counter()
        for _ in range(10):
            engine.search("searchable", temp_dir)
        full_time = time.perf_counter() - start

        # files_only should not be significantly slower
        # (may not be faster due to small dataset)
        assert files_only_time <= full_time * 2
        engine.close()


# === Symbol Search Tests ===

class TestChainSymbolSearch:
    """Tests for chain symbol search."""

    def test_symbol_search_finds_across_dirs(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test symbol search finds symbols across directories."""
        structure = hierarchical_index_structure
        root_db = structure["root"]["db"]
        root_path = structure["root"]["path"]

        mock_mapper.source_to_index_db.return_value = root_db

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        symbols = engine.search_symbols("auth", root_path)

        # Should find authenticate and AuthManager
        assert len(symbols) >= 2
        engine.close()

    def test_symbol_search_with_kind_filter(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test symbol search with kind filter."""
        structure = hierarchical_index_structure
        root_db = structure["root"]["db"]
        root_path = structure["root"]["path"]

        mock_mapper.source_to_index_db.return_value = root_db

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        classes = engine.search_symbols("", root_path, kind="class")

        # Should find AuthManager and DatabasePool
        assert all(s.kind == "class" for s in classes)
        engine.close()

    def test_symbol_search_deduplicates(self, mock_registry, mock_mapper, temp_dir):
        """Test symbol search deduplicates by (name, kind, range) but keeps different ranges."""
        # Create two indexes with same symbol name but different ranges
        dir1 = temp_dir / "dir1"
        dir1.mkdir()
        db1 = dir1 / "_index.db"
        store1 = DirIndexStore(db1)
        store1.initialize()
        store1.add_file(
            name="a.py",
            full_path=str(dir1 / "a.py"),
            content="def foo(): pass",
            language="python",
            symbols=[Symbol(name="foo", kind="function", range=(1, 5))],  # Different range
        )

        dir2 = temp_dir / "dir2"
        dir2.mkdir()
        db2 = dir2 / "_index.db"
        store2 = DirIndexStore(db2)
        store2.initialize()
        store2.add_file(
            name="b.py",
            full_path=str(dir2 / "b.py"),
            content="def foo(): pass\n# more code\n",
            language="python",
            symbols=[Symbol(name="foo", kind="function", range=(1, 10))],  # Different range
        )
        store2.close()

        # Register subdir after dir2 is created
        store1.register_subdir(name="dir2", index_path=db2)
        store1.close()

        mock_mapper.source_to_index_db.return_value = db1

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        symbols = engine.search_symbols("foo", dir1)

        # Should have exactly 2 (different ranges make them unique)
        assert len(symbols) == 2
        engine.close()


# === Search Options Tests ===

class TestSearchOptionsExtended:
    """Extended tests for SearchOptions."""

    def test_include_semantic_option(self):
        """Test include_semantic option."""
        options = SearchOptions(include_semantic=True)
        assert options.include_semantic is True

        options_default = SearchOptions()
        assert options_default.include_semantic is False

    def test_all_options_combined(self):
        """Test all options set together."""
        options = SearchOptions(
            depth=5,
            max_workers=16,
            limit_per_dir=20,
            total_limit=200,
            include_symbols=True,
            files_only=True,
            include_semantic=True,
        )
        assert options.depth == 5
        assert options.max_workers == 16
        assert options.limit_per_dir == 20
        assert options.total_limit == 200
        assert options.include_symbols is True
        assert options.files_only is True
        assert options.include_semantic is True

    def test_options_with_zero_values(self):
        """Test options with zero values."""
        options = SearchOptions(
            depth=0,
            max_workers=1,
            limit_per_dir=1,
            total_limit=1,
        )
        assert options.depth == 0
        assert options.max_workers == 1
        assert options.limit_per_dir == 1
        assert options.total_limit == 1


# === Quick Search Tests ===

class TestQuickSearch:
    """Tests for quick_search convenience function."""

    def test_quick_search_returns_results(self, temp_dir):
        """Test quick_search returns SearchResult list."""
        # Setup: Create index at a known location
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(
            name="test.py",
            full_path=str(temp_dir / "test.py"),
            content="searchable content for quick search test",
            language="python",
        )
        store.close()

        # Test requires actual registry - skip if not initialized
        try:
            results = quick_search("searchable", temp_dir)
            assert isinstance(results, list)
        except Exception:
            # May fail if registry not properly set up
            pytest.skip("Registry not available for quick_search test")

    def test_quick_search_with_depth(self, temp_dir):
        """Test quick_search respects depth parameter."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(
            name="test.py",
            full_path=str(temp_dir / "test.py"),
            content="test content",
            language="python",
        )
        store.close()

        try:
            results = quick_search("test", temp_dir, depth=0)
            assert isinstance(results, list)
        except Exception:
            pytest.skip("Registry not available for quick_search test")


# === Edge Cases and Error Handling ===

class TestSearchErrorHandling:
    """Tests for search error handling."""

    def test_search_corrupted_index(self, mock_registry, mock_mapper, temp_dir):
        """Test search handles corrupted index gracefully."""
        # Create corrupted index file
        db_path = temp_dir / "_index.db"
        db_path.write_text("not a valid sqlite database")

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        try:
            result = engine.search("test", temp_dir)
            # Should return empty results, not crash
            assert result.results == []
        finally:
            engine.close()
            # Force cleanup on Windows
            import gc
            gc.collect()

    def test_search_empty_index(self, mock_registry, mock_mapper, temp_dir):
        """Test search on empty index returns empty results."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        result = engine.search("anything", temp_dir)

        assert result.results == []
        assert result.stats.files_matched == 0
        engine.close()

    def test_search_special_fts_characters(self, mock_registry, mock_mapper, temp_dir):
        """Test search handles FTS5 special characters."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(
            name="test.py",
            full_path=str(temp_dir / "test.py"),
            content="test content",
            language="python",
        )
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)

        # These should not crash
        special_queries = [
            "test*",
            "test OR other",
            '"exact phrase"',
            "NOT invalid",
        ]

        for query in special_queries:
            result = engine.search(query, temp_dir)
            assert isinstance(result.results, list)

        engine.close()


# === Concurrent Search Tests ===

class TestConcurrentSearch:
    """Tests for concurrent search operations."""

    def test_multiple_concurrent_searches(self, mock_registry, mock_mapper, temp_dir):
        """Test multiple concurrent searches don't interfere."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        for i in range(10):
            store.add_file(
                name=f"file{i}.py",
                full_path=str(temp_dir / f"file{i}.py"),
                content=f"content{i} searchable data",
                language="python",
            )
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)

        results = []
        errors = []

        def search_task(query):
            try:
                r = engine.search(query, temp_dir)
                results.append(len(r.results))
            except Exception as e:
                errors.append(str(e))

        threads = [
            threading.Thread(target=search_task, args=(f"content{i}",))
            for i in range(5)
        ]

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        assert len(results) == 5
        engine.close()

    def test_search_during_close(self, mock_registry, mock_mapper, temp_dir):
        """Test behavior when search happens during close."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(
            name="test.py",
            full_path=str(temp_dir / "test.py"),
            content="test content",
            language="python",
        )
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)

        # Start a search then immediately close
        result = engine.search("test", temp_dir)
        engine.close()

        # Should complete without error
        assert isinstance(result.results, list)


# === Search Statistics Tests ===

class TestSearchStatsExtended:
    """Extended tests for search statistics."""

    def test_stats_time_is_positive(self, mock_registry, mock_mapper, temp_dir):
        """Test search time is recorded and positive."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(
            name="test.py",
            full_path=str(temp_dir / "test.py"),
            content="test content",
            language="python",
        )
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        result = engine.search("test", temp_dir)

        assert result.stats.time_ms >= 0
        engine.close()

    def test_stats_dirs_searched_accurate(self, mock_registry, mock_mapper, hierarchical_index_structure):
        """Test dirs_searched count is accurate."""
        structure = hierarchical_index_structure
        root_db = structure["root"]["db"]
        root_path = structure["root"]["path"]

        mock_mapper.source_to_index_db.return_value = root_db

        engine = ChainSearchEngine(mock_registry, mock_mapper)

        # Depth 0
        result0 = engine.search("test", root_path, SearchOptions(depth=0))
        assert result0.stats.dirs_searched == 1

        # Depth 1
        result1 = engine.search("test", root_path, SearchOptions(depth=1))
        assert result1.stats.dirs_searched == 3  # root + src + tests

        # Unlimited
        result_all = engine.search("test", root_path, SearchOptions(depth=-1))
        assert result_all.stats.dirs_searched == 5

        engine.close()

    def test_stats_files_matched_accurate(self, mock_registry, mock_mapper, temp_dir):
        """Test files_matched count is accurate."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()

        # Add files with different content
        store.add_file(name="match1.py", full_path=str(temp_dir / "match1.py"),
                       content="findme keyword", language="python")
        store.add_file(name="match2.py", full_path=str(temp_dir / "match2.py"),
                       content="findme keyword", language="python")
        store.add_file(name="nomatch.py", full_path=str(temp_dir / "nomatch.py"),
                       content="other content", language="python")
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        result = engine.search("findme", temp_dir)

        assert result.stats.files_matched == 2
        engine.close()


# === Boundary Condition Tests ===

class TestBoundaryConditions:
    """Tests for boundary conditions."""

    def test_search_with_max_workers_one(self, mock_registry, mock_mapper, temp_dir):
        """Test search with single worker."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(name="test.py", full_path=str(temp_dir / "test.py"),
                       content="test content", language="python")
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper, max_workers=1)
        result = engine.search("test", temp_dir, SearchOptions(max_workers=1))

        assert isinstance(result.results, list)
        engine.close()

    def test_search_with_limit_one(self, mock_registry, mock_mapper, temp_dir):
        """Test search with limit=1."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        for i in range(10):
            store.add_file(name=f"file{i}.py", full_path=str(temp_dir / f"file{i}.py"),
                           content="searchable content", language="python")
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        result = engine.search("searchable", temp_dir, SearchOptions(total_limit=1))

        assert len(result.results) <= 1
        engine.close()

    def test_search_very_long_query(self, mock_registry, mock_mapper, temp_dir):
        """Test search with very long query."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(name="test.py", full_path=str(temp_dir / "test.py"),
                       content="test content", language="python")
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)

        # Very long query
        long_query = " ".join(["word"] * 100)
        result = engine.search(long_query, temp_dir)

        # Should not crash
        assert isinstance(result.results, list)
        engine.close()

    def test_search_unicode_query(self, mock_registry, mock_mapper, temp_dir):
        """Test search with unicode query does not crash."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(
            name="unicode.py",
            full_path=str(temp_dir / "unicode.py"),
            content="# Chinese comment\ndef hello(): return 'hello world'",
            language="python",
        )
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        # Unicode query should not crash (may or may not find results depending on FTS5 tokenizer)
        result = engine.search("hello", temp_dir)

        assert isinstance(result.results, list)
        assert len(result.results) >= 1
        engine.close()

    def test_search_empty_directory(self, mock_registry, mock_mapper, temp_dir):
        """Test search in directory with no files."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        # Don't add any files
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)
        result = engine.search("anything", temp_dir)

        assert result.results == []
        assert result.stats.files_matched == 0
        engine.close()


# === Include Symbols Option Tests ===

class TestIncludeSymbolsOption:
    """Tests for include_symbols search option."""

    def test_search_with_include_symbols(self, mock_registry, mock_mapper, temp_dir):
        """Test search returns symbols when include_symbols=True."""
        db_path = temp_dir / "_index.db"
        store = DirIndexStore(db_path)
        store.initialize()
        store.add_file(
            name="test.py",
            full_path=str(temp_dir / "test.py"),
            content="def my_function(): pass",
            language="python",
            symbols=[Symbol(name="my_function", kind="function", range=(1, 1))],
        )
        store.close()

        mock_mapper.source_to_index_db.return_value = db_path

        engine = ChainSearchEngine(mock_registry, mock_mapper)

        # Without include_symbols
        result_no_symbols = engine.search("function", temp_dir, SearchOptions(include_symbols=False))
        assert result_no_symbols.symbols == []

        # With include_symbols
        result_with_symbols = engine.search("function", temp_dir, SearchOptions(include_symbols=True))
        # Symbols list populated (may or may not match depending on implementation)
        assert isinstance(result_with_symbols.symbols, list)

        engine.close()


# === ChainSearchResult Tests ===

class TestChainSearchResultExtended:
    """Extended tests for ChainSearchResult dataclass."""

    def test_result_immutability(self):
        """Test ChainSearchResult fields."""
        stats = SearchStats(dirs_searched=5, files_matched=10, time_ms=100.5)
        results = [SearchResult(path="/test.py", score=1.0, excerpt="test")]
        symbols = [Symbol(name="foo", kind="function", range=(1, 5))]

        result = ChainSearchResult(
            query="test query",
            results=results,
            symbols=symbols,
            stats=stats,
        )

        assert result.query == "test query"
        assert len(result.results) == 1
        assert len(result.symbols) == 1
        assert result.related_results == []
        assert result.stats.dirs_searched == 5

    def test_result_with_empty_collections(self):
        """Test ChainSearchResult with empty results and symbols."""
        result = ChainSearchResult(
            query="no matches",
            results=[],
            symbols=[],
            stats=SearchStats(),
        )

        assert result.query == "no matches"
        assert result.results == []
        assert result.related_results == []
        assert result.symbols == []
        assert result.stats.dirs_searched == 0
