"""Benchmark tests for search operations (grep, regex matching).

Run with: pytest tests/benchmarks/test_search_benchmarks.py -v --benchmark-only

These benchmarks compare Python regex vs Rust grep implementation.
See issue #570 for context.
"""

import re

import pytest


def generate_log_content(num_lines: int) -> bytes:
    """Generate realistic log file content for grep benchmarks."""
    lines = []
    for i in range(num_lines):
        if i % 10 == 0:
            lines.append(f"[ERROR] 2024-01-15 10:30:{i:02d} - Failed to connect to database")
        elif i % 5 == 0:
            lines.append(f"[WARN] 2024-01-15 10:30:{i:02d} - Slow query detected: {i}ms")
        else:
            lines.append(f"[INFO] 2024-01-15 10:30:{i:02d} - Request processed successfully")
    return "\n".join(lines).encode("utf-8")


def generate_code_content(num_lines: int) -> bytes:
    """Generate Python-like code content for grep benchmarks."""
    lines = []
    for i in range(num_lines):
        if i % 20 == 0:
            lines.append(f"class MyClass{i}:")
        elif i % 10 == 0:
            lines.append(f"    def method_{i}(self, arg: str) -> int:")
        elif i % 5 == 0:
            lines.append(f"        # TODO: implement this function #{i}")
        else:
            lines.append(f"        return {i}")
    return "\n".join(lines).encode("utf-8")


# =============================================================================
# PYTHON REGEX BENCHMARKS (baseline)
# =============================================================================


@pytest.mark.benchmark_hash
class TestPythonRegexBenchmarks:
    """Baseline benchmarks using Python's re module."""

    def test_python_regex_simple_1k_lines(self, benchmark):
        """Benchmark Python regex search in 1K lines."""
        content = generate_log_content(1000)
        content_str = content.decode("utf-8")
        pattern = re.compile(r"ERROR")

        def search():
            return pattern.findall(content_str)

        result = benchmark(search)
        assert len(result) == 100  # 1 ERROR per 10 lines

    @pytest.mark.benchmark_ci
    def test_python_regex_simple_10k_lines(self, benchmark):
        """Benchmark Python regex search in 10K lines."""
        content = generate_log_content(10000)
        content_str = content.decode("utf-8")
        pattern = re.compile(r"ERROR")

        def search():
            return pattern.findall(content_str)

        result = benchmark(search)
        assert len(result) == 1000

    def test_python_regex_complex_pattern(self, benchmark):
        """Benchmark Python regex with complex pattern."""
        content = generate_code_content(5000)
        content_str = content.decode("utf-8")
        # Match function definitions
        pattern = re.compile(r"def\s+\w+\(.*?\)")

        def search():
            return pattern.findall(content_str)

        result = benchmark(search)
        assert len(result) > 0

    def test_python_regex_line_by_line(self, benchmark):
        """Benchmark Python regex searching line by line (like grep)."""
        content = generate_log_content(5000)
        lines = content.decode("utf-8").split("\n")
        pattern = re.compile(r"ERROR")

        def search():
            matches = []
            for i, line in enumerate(lines):
                if pattern.search(line):
                    matches.append((i + 1, line))
            return matches

        result = benchmark(search)
        assert len(result) == 500

    def test_python_regex_case_insensitive(self, benchmark):
        """Benchmark Python regex with case-insensitive flag."""
        content = generate_log_content(5000)
        content_str = content.decode("utf-8")
        pattern = re.compile(r"error", re.IGNORECASE)

        def search():
            return pattern.findall(content_str)

        result = benchmark(search)
        assert len(result) == 500


# =============================================================================
# RUST GREP BENCHMARKS
# =============================================================================


@pytest.mark.benchmark_hash
class TestRustGrepBenchmarks:
    """Benchmarks for Rust-accelerated grep operations."""

    def test_rust_grep_available(self):
        """Check if Rust grep is available."""
        RUST_AVAILABLE = True

        print(f"\n[INFO] Rust grep available: {RUST_AVAILABLE}")

    def test_rust_grep_1k_lines(self, benchmark):
        """Benchmark Rust grep in 1K lines."""
        from nexus._rust_compat import grep_bulk

        RUST_AVAILABLE = True

        content = generate_log_content(1000)
        file_contents = {"/test.log": content}

        def search():
            result = grep_bulk("ERROR", file_contents, ignore_case=False)
            if result is None:
                # Fallback to Python if Rust not available

                matches = []
                content_str = content.decode("utf-8")
                for i, line in enumerate(content_str.split("\n")):
                    if "ERROR" in line:
                        matches.append({"file": "/test.log", "line": i + 1, "content": line})
                return matches
            return result

        result = benchmark(search)
        assert len(result) == 100

        if RUST_AVAILABLE:
            print("\n[INFO] Rust acceleration was used")
        else:
            print("\n[INFO] Python fallback was used")

    @pytest.mark.benchmark_ci
    def test_rust_grep_10k_lines(self, benchmark):
        """Benchmark Rust grep in 10K lines."""
        from nexus._rust_compat import grep_bulk

        content = generate_log_content(10000)
        file_contents = {"/test.log": content}

        def search():
            result = grep_bulk("ERROR", file_contents, ignore_case=False)
            if result is None:
                matches = []
                content_str = content.decode("utf-8")
                for i, line in enumerate(content_str.split("\n")):
                    if "ERROR" in line:
                        matches.append({"file": "/test.log", "line": i + 1, "content": line})
                return matches
            return result

        result = benchmark(search)
        assert len(result) == 1000

    def test_rust_grep_multiple_files(self, benchmark):
        """Benchmark Rust grep across multiple files."""
        from nexus._rust_compat import grep_bulk

        # Create 10 files with 1K lines each
        file_contents = {f"/file_{i}.log": generate_log_content(1000) for i in range(10)}

        def search():
            result = grep_bulk("ERROR", file_contents, ignore_case=False)
            if result is None:
                matches = []
                for path, content in file_contents.items():
                    content_str = content.decode("utf-8")
                    for i, line in enumerate(content_str.split("\n")):
                        if "ERROR" in line:
                            matches.append({"file": path, "line": i + 1, "content": line})
                return matches
            return result

        result = benchmark(search)
        assert len(result) == 1000  # 100 per file * 10 files

    def test_rust_grep_regex_pattern(self, benchmark):
        """Benchmark Rust grep with regex pattern."""
        from nexus._rust_compat import grep_bulk

        content = generate_code_content(5000)
        file_contents = {"/code.py": content}

        def search():
            result = grep_bulk(r"def\s+\w+", file_contents, ignore_case=False)
            if result is None:
                import re

                pattern = re.compile(r"def\s+\w+")
                matches = []
                content_str = content.decode("utf-8")
                for i, line in enumerate(content_str.split("\n")):
                    if pattern.search(line):
                        matches.append({"file": "/code.py", "line": i + 1, "content": line})
                return matches
            return result

        result = benchmark(search)
        assert len(result) > 0

    def test_rust_grep_case_insensitive(self, benchmark):
        """Benchmark Rust grep with case-insensitive search."""
        from nexus._rust_compat import grep_bulk

        content = generate_log_content(5000)
        file_contents = {"/test.log": content}

        def search():
            result = grep_bulk("error", file_contents, ignore_case=True)
            if result is None:
                import re

                pattern = re.compile("error", re.IGNORECASE)
                matches = []
                content_str = content.decode("utf-8")
                for i, line in enumerate(content_str.split("\n")):
                    if pattern.search(line):
                        matches.append({"file": "/test.log", "line": i + 1, "content": line})
                return matches
            return result

        result = benchmark(search)
        assert len(result) == 500


# =============================================================================
# RUST MMAP GREP BENCHMARKS (Issue #893)
# =============================================================================


@pytest.mark.benchmark_hash
class TestRustMmapGrepBenchmarks:
    """Benchmarks for Rust mmap-accelerated grep operations (Issue #893)."""

    @pytest.fixture(autouse=True)
    def setup_temp_files(self, tmp_path):
        """Create temporary files for mmap benchmarks."""
        self.tmp_path = tmp_path
        self.test_files = {}

        # Create test files with various sizes
        for i in range(10):
            content = generate_log_content(1000)
            file_path = tmp_path / f"file_{i}.log"
            file_path.write_bytes(content)
            self.test_files[str(file_path)] = content

        # Create a large file for mmap performance testing
        large_content = generate_log_content(10000)
        large_file = tmp_path / "large_file.log"
        large_file.write_bytes(large_content)
        self.large_file = str(large_file)
        self.large_content = large_content

    def test_mmap_grep_available(self):
        """Check if mmap grep is available."""
        MMAP_AVAILABLE = True

        print(f"\n[INFO] Mmap grep available: {MMAP_AVAILABLE}")

    def test_mmap_grep_single_file(self, benchmark):
        """Benchmark mmap grep on a single file."""
        from nexus._rust_compat import grep_files_mmap

        MMAP_AVAILABLE = True

        def search():
            result = grep_files_mmap("ERROR", [self.large_file], ignore_case=False)
            if result is None:
                # Fallback to Python if mmap not available
                import re

                pattern = re.compile(r"ERROR")
                matches = []
                content_str = self.large_content.decode("utf-8")
                for i, line in enumerate(content_str.split("\n")):
                    if pattern.search(line):
                        matches.append({"file": self.large_file, "line": i + 1, "content": line})
                return matches
            return result

        result = benchmark(search)
        assert len(result) == 1000  # 1 ERROR per 10 lines in 10000 lines

        if MMAP_AVAILABLE:
            print("\n[INFO] Mmap acceleration was used")
        else:
            print("\n[INFO] Python fallback was used")

    def test_mmap_grep_multiple_files(self, benchmark):
        """Benchmark mmap grep across multiple files."""
        from nexus._rust_compat import grep_files_mmap

        MMAP_AVAILABLE = True

        file_paths = list(self.test_files.keys())

        def search():
            result = grep_files_mmap("ERROR", file_paths, ignore_case=False)
            if result is None:
                # Fallback to Python if mmap not available
                import re

                pattern = re.compile(r"ERROR")
                matches = []
                for path, content in self.test_files.items():
                    content_str = content.decode("utf-8")
                    for i, line in enumerate(content_str.split("\n")):
                        if pattern.search(line):
                            matches.append({"file": path, "line": i + 1, "content": line})
                return matches
            return result

        result = benchmark(search)
        assert len(result) == 1000  # 100 ERRORs per file * 10 files

        if MMAP_AVAILABLE:
            print("\n[INFO] Mmap acceleration was used")
        else:
            print("\n[INFO] Python fallback was used")

    def test_mmap_vs_bulk_grep_comparison(self, benchmark):
        """Compare mmap grep vs bulk grep (read + grep)."""
        from nexus._rust_compat import grep_bulk, grep_files_mmap

        MMAP_AVAILABLE = True

        file_paths = list(self.test_files.keys())

        def search_mmap():
            """Use mmap-based grep (zero-copy)."""
            result = grep_files_mmap("ERROR", file_paths, ignore_case=False)
            return result if result else []

        def search_bulk():
            """Use bulk grep (read + copy)."""
            # Read files into memory first
            file_contents = {}
            for path in file_paths:
                with open(path, "rb") as f:
                    file_contents[path] = f.read()
            result = grep_bulk("ERROR", file_contents, ignore_case=False)
            return result if result else []

        if MMAP_AVAILABLE:
            result = benchmark(search_mmap)
            print("\n[INFO] Benchmarking mmap grep")
        else:
            result = benchmark(search_bulk)
            print("\n[INFO] Benchmarking bulk grep (mmap not available)")

        assert len(result) == 1000

    def test_mmap_grep_case_insensitive(self, benchmark):
        """Benchmark mmap grep with case-insensitive search."""
        from nexus._rust_compat import grep_files_mmap

        def search():
            result = grep_files_mmap("error", [self.large_file], ignore_case=True)
            if result is None:
                import re

                pattern = re.compile("error", re.IGNORECASE)
                matches = []
                content_str = self.large_content.decode("utf-8")
                for i, line in enumerate(content_str.split("\n")):
                    if pattern.search(line):
                        matches.append({"file": self.large_file, "line": i + 1, "content": line})
                return matches
            return result

        result = benchmark(search)
        assert len(result) == 1000

    def test_mmap_grep_regex_pattern(self, benchmark):
        """Benchmark mmap grep with regex pattern."""
        from nexus._rust_compat import grep_files_mmap

        # Create code files for regex testing
        code_content = generate_code_content(5000)
        code_file = self.tmp_path / "code.py"
        code_file.write_bytes(code_content)

        def search():
            result = grep_files_mmap(r"def\s+\w+", [str(code_file)], ignore_case=False)
            if result is None:
                import re

                pattern = re.compile(r"def\s+\w+")
                matches = []
                content_str = code_content.decode("utf-8")
                for i, line in enumerate(content_str.split("\n")):
                    if pattern.search(line):
                        matches.append({"file": str(code_file), "line": i + 1, "content": line})
                return matches
            return result

        result = benchmark(search)
        assert len(result) > 0


# =============================================================================
# GLOB PATTERN MATCHING BENCHMARKS
# =============================================================================


@pytest.mark.benchmark_glob
class TestGlobPatternBenchmarks:
    """Benchmarks for glob pattern matching."""

    def test_python_fnmatch_simple(self, benchmark):
        """Benchmark Python fnmatch for simple patterns."""
        import fnmatch

        paths = [f"/dir/file_{i:04d}.txt" for i in range(1000)]
        paths += [f"/dir/file_{i:04d}.py" for i in range(1000)]

        def match():
            return [p for p in paths if fnmatch.fnmatch(p, "*.txt")]

        result = benchmark(match)
        assert len(result) == 1000

    def test_python_fnmatch_complex(self, benchmark):
        """Benchmark Python fnmatch for complex patterns."""
        import fnmatch

        paths = [f"/src/module_{i}/file_{j}.py" for i in range(50) for j in range(20)]

        def match():
            return [p for p in paths if fnmatch.fnmatch(p, "/src/module_*/file_*.py")]

        result = benchmark(match)
        assert len(result) == 1000

    def test_rust_glob_simple(self, benchmark):
        """Benchmark Rust glob for simple patterns (if available)."""
        from nexus._rust_compat import glob_match_bulk

        RUST_AVAILABLE = True

        paths = [f"/dir/file_{i:04d}.txt" for i in range(1000)]
        paths += [f"/dir/file_{i:04d}.py" for i in range(1000)]

        def match():
            result = glob_match_bulk(["*.txt"], paths)
            if result is None:
                import fnmatch

                return [p for p in paths if fnmatch.fnmatch(p, "*.txt")]
            return result

        result = benchmark(match)
        assert len(result) == 1000

        if RUST_AVAILABLE:
            print("\n[INFO] Rust glob was used")
        else:
            print("\n[INFO] Python fallback was used (Rust glob not available)")

    def test_rust_glob_multiple_patterns(self, benchmark):
        """Benchmark Rust glob with multiple patterns."""
        from nexus._rust_compat import glob_match_bulk

        paths = [f"/dir/file_{i:04d}.txt" for i in range(500)]
        paths += [f"/dir/file_{i:04d}.py" for i in range(500)]
        paths += [f"/dir/file_{i:04d}.json" for i in range(500)]
        paths += [f"/dir/file_{i:04d}.md" for i in range(500)]

        def match():
            result = glob_match_bulk(["*.txt", "*.py"], paths)
            if result is None:
                import fnmatch

                return [
                    p for p in paths if fnmatch.fnmatch(p, "*.txt") or fnmatch.fnmatch(p, "*.py")
                ]
            return result

        result = benchmark(match)
        assert len(result) == 1000

    def test_rust_glob_recursive_pattern(self, benchmark):
        """Benchmark Rust glob with recursive pattern (**/*)."""
        from nexus._rust_compat import glob_match_bulk

        # Generate paths with directory structure
        paths = []
        for i in range(10):
            for j in range(10):
                for k in range(10):
                    paths.append(f"/level_{i}/level_{j}/level_{k}/file.py")

        def match():
            result = glob_match_bulk(["**/*.py"], paths)
            if result is None:
                # Fallback: all paths match since they all end in .py
                return paths
            return result

        result = benchmark(match)
        assert len(result) == 1000


# =============================================================================
# TRIGRAM INDEX BENCHMARKS (Issue #954)
# =============================================================================


@pytest.mark.benchmark_hash
class TestTrigramBenchmarks:
    """Benchmarks for trigram index build and search operations (Issue #954)."""

    @pytest.fixture(autouse=True)
    def setup_trigram_files(self, tmp_path):
        """Create temporary files for trigram benchmarks."""
        self.tmp_path = tmp_path

        # Create 1K files with varying content
        self.file_paths_1k = []
        for i in range(1000):
            content = generate_log_content(100)
            file_path = tmp_path / f"file_{i:04d}.log"
            file_path.write_bytes(content)
            self.file_paths_1k.append(str(file_path))

        # Create 100 code files
        self.code_files = []
        for i in range(100):
            content = generate_code_content(200)
            file_path = tmp_path / f"code_{i:04d}.py"
            file_path.write_bytes(content)
            self.code_files.append(str(file_path))

    def test_trigram_build_1k_files(self, benchmark):
        """Benchmark building trigram index from 1K files."""
        from nexus.bricks.search.primitives import trigram_fast

        if not trigram_fast.is_available():
            pytest.skip("Trigram index not available")

        idx_path = str(self.tmp_path / "bench_1k.trgm")

        def build():
            trigram_fast.build_index(self.file_paths_1k, idx_path)

        benchmark(build)

        stats = trigram_fast.get_stats(idx_path)
        assert stats is not None
        print(
            f"\n[INFO] 1K files index: {stats['file_count']} files, "
            f"{stats['trigram_count']} trigrams, "
            f"{stats['index_size_bytes'] / 1024:.1f} KB"
        )

    def test_trigram_search_literal(self, benchmark):
        """Benchmark trigram search for literal pattern."""
        from nexus.bricks.search.primitives import trigram_fast

        if not trigram_fast.is_available():
            pytest.skip("Trigram index not available")

        idx_path = str(self.tmp_path / "bench_search.trgm")
        trigram_fast.build_index(self.file_paths_1k, idx_path)

        def search():
            return trigram_fast.grep(idx_path, "ERROR", max_results=1000)

        result = benchmark(search)
        assert result is not None
        print(f"\n[INFO] Trigram literal search: {len(result)} matches")

    def test_trigram_search_regex(self, benchmark):
        """Benchmark trigram search for regex pattern."""
        from nexus.bricks.search.primitives import trigram_fast

        if not trigram_fast.is_available():
            pytest.skip("Trigram index not available")

        idx_path = str(self.tmp_path / "bench_regex.trgm")
        trigram_fast.build_index(self.code_files, idx_path)

        def search():
            return trigram_fast.grep(idx_path, r"def\s+\w+", max_results=1000)

        result = benchmark(search)
        assert result is not None
        print(f"\n[INFO] Trigram regex search: {len(result)} matches")

    def test_trigram_search_no_match(self, benchmark):
        """Benchmark trigram search for non-matching pattern."""
        from nexus.bricks.search.primitives import trigram_fast

        if not trigram_fast.is_available():
            pytest.skip("Trigram index not available")

        idx_path = str(self.tmp_path / "bench_nomatch.trgm")
        trigram_fast.build_index(self.file_paths_1k, idx_path)

        def search():
            return trigram_fast.grep(idx_path, "xyzzy_nonexistent_12345", max_results=1000)

        result = benchmark(search)
        assert result is not None
        assert len(result) == 0
        print("\n[INFO] Trigram no-match: 0 results (fast rejection)")

    def test_trigram_vs_mmap_grep(self, benchmark):
        """Compare trigram index search vs mmap grep."""
        from nexus.bricks.search.primitives import trigram_fast

        if not trigram_fast.is_available():
            pytest.skip("Trigram index not available")

        idx_path = str(self.tmp_path / "bench_compare.trgm")
        trigram_fast.build_index(self.file_paths_1k, idx_path)

        def search_trigram():
            return trigram_fast.grep(idx_path, "ERROR", max_results=1000)

        result = benchmark(search_trigram)
        assert result is not None
        print(f"\n[INFO] Trigram search: {len(result)} matches")


# =============================================================================
# files=[...] VALIDATOR BENCHMARKS (#3701 Issue 13A)
#
# These benchmarks document the cost of the caller-supplied files
# validator used by grep/glob when ``files=[...]`` is set. The
# FILES_FILTER_TRIGRAM_THRESHOLD constant (200) was chosen as a
# conservative crossover point between direct-scan and
# trigram+post-filter — a full end-to-end crossover benchmark would
# require a real trigram index on a realistic corpus, which is out of
# scope for this file. Production operators should tune the threshold
# by profiling their own workloads.
# =============================================================================


def _stub_search_service(list_return: list[str]):
    """Build a SearchService with just enough wiring to exercise the validator.

    Uses ``patch.object`` so the stub overrides ``list`` and
    ``_get_routing_params`` without assigning to bound-method
    attributes (mypy rejects that). Caller must stop the returned
    patches in a ``finally`` block.
    """
    from unittest.mock import MagicMock, patch

    from nexus.bricks.search.search_service import SearchService

    svc = SearchService(metadata_store=MagicMock())
    list_patch = patch.object(svc, "list", return_value=list_return)
    routing_patch = patch.object(svc, "_get_routing_params", return_value=(None, None, False))
    list_patch.start()
    routing_patch.start()
    return svc, (list_patch, routing_patch)


@pytest.mark.benchmark_hash
class TestFilesFilterValidator:
    """Benchmark the validator's overhead at realistic list sizes."""

    def test_validator_small_list(self, benchmark):
        """Typical agent narrowing: 10 files."""
        svc, patches = _stub_search_service(list_return=[f"/f{i}.py" for i in range(50)])
        try:
            files = [f"/f{i}.py" for i in range(10)]

            def run():
                return svc._validate_and_normalize_files(files, path="/", context=None)

            result = benchmark(run)
            assert len(result[0]) == 10
        finally:
            for p in patches:
                p.stop()

    def test_validator_at_threshold_size(self, benchmark):
        """At the trigram-bypass threshold: 200 files."""
        from nexus.bricks.search.search_service import FILES_FILTER_TRIGRAM_THRESHOLD

        svc, patches = _stub_search_service(
            list_return=[f"/f{i}.py" for i in range(FILES_FILTER_TRIGRAM_THRESHOLD * 2)]
        )
        try:
            files = [f"/f{i}.py" for i in range(FILES_FILTER_TRIGRAM_THRESHOLD)]

            def run():
                return svc._validate_and_normalize_files(files, path="/", context=None)

            result = benchmark(run)
            assert len(result[0]) == FILES_FILTER_TRIGRAM_THRESHOLD
        finally:
            for p in patches:
                p.stop()

    def test_validator_at_size_cap(self, benchmark):
        """Stress: validator at FILES_FILTER_SIZE_CAP = 10k entries."""
        from nexus.bricks.search.search_service import FILES_FILTER_SIZE_CAP

        svc, patches = _stub_search_service(
            list_return=[f"/f{i}.py" for i in range(FILES_FILTER_SIZE_CAP)]
        )
        try:
            files = [f"/f{i}.py" for i in range(FILES_FILTER_SIZE_CAP)]

            def run():
                return svc._validate_and_normalize_files(files, path="/", context=None)

            result = benchmark(run)
            assert len(result[0]) == FILES_FILTER_SIZE_CAP
        finally:
            for p in patches:
                p.stop()
