"""
Concurrent Indexing Tests

This module tests the thread safety and behavior of concurrent indexing operations.

TEST COVERAGE:
- Multiple simultaneous index operations
- Cache invalidation during concurrent operations
- Race condition detection
- Lock contention analysis
- Performance under load
"""

import pytest
import asyncio
import os
import tempfile
import shutil
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Dict, Any
from pathlib import Path

from ..file_stat_cache import FileStatCache
from ..parallel_scanner import ParallelScanner
from ..validation import validate_file_path
from ..logger_config import logger


class TestConcurrentIndexing:
    """
    Test suite for concurrent indexing operations.

    These tests verify that:
    1. Multiple index operations can run simultaneously without data corruption
    2. Cache invalidation works correctly under concurrent access
    3. No deadlocks occur with proper locking
    4. Performance scales reasonably with concurrency
    """

    @pytest.fixture
    def temp_dir(self):
        """Create a temporary directory for testing."""
        temp_dir = tempfile.mkdtemp()
        yield temp_dir
        shutil.rmtree(temp_dir)

    @pytest.fixture
    def test_files(self, temp_dir):
        """Create test files for indexing."""
        # Create a directory structure with test files
        test_files = []

        # Create some test files
        for i in range(10):
            file_path = os.path.join(temp_dir, f"test_{i}.txt")
            with open(file_path, 'w') as f:
                f.write(f"Test content {i}\n")
            test_files.append(file_path)

        # Create subdirectories with files
        subdir = os.path.join(temp_dir, "subdir")
        os.makedirs(subdir)
        for i in range(5):
            file_path = os.path.join(subdir, f"sub_test_{i}.txt")
            with open(file_path, 'w') as f:
                f.write(f"Sub test content {i}\n")
            test_files.append(file_path)

        return test_files

    def test_concurrent_cache_access(self, test_files):
        """
        Test that multiple threads can access the cache safely.

        SCENARIO:
        - 10 threads simultaneously reading/writing to cache
        - Each thread performs 100 operations
        - Verify no exceptions and consistent data
        """
        cache = FileStatCache(max_size=1000)
        num_threads = 10
        operations_per_thread = 100
        errors = []

        def cache_worker(thread_id: int):
            """Worker function that performs cache operations."""
            try:
                for i in range(operations_per_thread):
                    # Alternate between different files
                    file_path = test_files[i % len(test_files)]

                    # Get stat (may be cache hit or miss)
                    stat_info = cache.get_stat(file_path)

                    # Verify consistency
                    if stat_info:
                        assert stat_info.path == file_path
                        assert stat_info.size >= 0
                        assert stat_info.mtime > 0

                    # Occasionally invalidate
                    if i % 10 == 0:
                        cache.invalidate(file_path)

            except Exception as e:
                errors.append(f"Thread {thread_id}: {e}")

        # Run threads concurrently
        with ThreadPoolExecutor(max_workers=num_threads) as executor:
            futures = [
                executor.submit(cache_worker, i)
                for i in range(num_threads)
            ]

            # Wait for all to complete
            for future in as_completed(futures):
                future.result()

        # Verify no errors occurred
        assert len(errors) == 0, f"Errors occurred: {errors}"

        # Verify cache statistics
        stats = cache.get_stats()
        assert stats['hits'] >= 0
        assert stats['misses'] >= 0
        assert stats['total_lookups'] == num_threads * operations_per_thread

    def test_concurrent_invalidation(self, test_files):
        """
        Test cache invalidation under concurrent access.

        SCENARIO:
        - Multiple threads reading from cache
        - One thread continuously invalidating entries
        - Verify cache remains consistent
        """
        cache = FileStatCache(max_size=100)
        num_readers = 5
        operations_per_reader = 50
        errors = []
        stop_event = threading.Event()

        def invalidation_worker():
            """Worker that continuously invalidates cache entries."""
            while not stop_event.is_set():
                for file_path in test_files:
                    cache.invalidate(file_path)
                time.sleep(0.01)  # Small delay

        def reader_worker(thread_id: int):
            """Worker that reads from cache."""
            try:
                for i in range(operations_per_reader):
                    file_path = test_files[i % len(test_files)]
                    stat_info = cache.get_stat(file_path)

                    # Verify consistency if we got a hit
                    if stat_info:
                        assert stat_info.path == file_path

            except Exception as e:
                errors.append(f"Reader thread {thread_id}: {e}")

        # Start invalidation thread
        inv_thread = threading.Thread(target=invalidation_worker)
        inv_thread.start()

        try:
            # Run reader threads
            with ThreadPoolExecutor(max_workers=num_readers) as executor:
                futures = [
                    executor.submit(reader_worker, i)
                    for i in range(num_readers)
                ]

                for future in as_completed(futures):
                    future.result()

        finally:
            # Stop invalidation thread
            stop_event.set()
            inv_thread.join(timeout=5.0)

        # Verify no errors
        assert len(errors) == 0, f"Errors occurred: {errors}"

    def test_parallel_scanner_concurrency(self, temp_dir):
        """
        Test parallel scanner with concurrent scans.

        SCENARIO:
        - Multiple parallel scans of the same directory
        - Verify no deadlocks or data corruption
        """
        # Create a larger directory structure
        for i in range(20):
            subdir = os.path.join(temp_dir, f"dir_{i}")
            os.makedirs(subdir)
            for j in range(10):
                file_path = os.path.join(subdir, f"file_{j}.txt")
                with open(file_path, 'w') as f:
                    f.write(f"Content {i}_{j}\n")

        async def run_concurrent_scans():
            """Run multiple scans concurrently."""
            scanner = ParallelScanner(max_workers=4, enable_symlink_protection=False)

            # Create multiple scan tasks
            tasks = [
                asyncio.create_task(scanner.scan(temp_dir))
                for _ in range(5)
            ]

            # Wait for all to complete
            results = await asyncio.gather(*tasks, return_exceptions=True)

            # Verify all succeeded
            for i, result in enumerate(results):
                assert isinstance(result, list), f"Scan {i} failed: {result}"
                assert len(result) > 0, f"Scan {i} returned empty results"

            return results

        # Run the concurrent scans
        results = asyncio.run(run_concurrent_scans())

        # All scans should return similar results
        first_result = results[0]
        for i, result in enumerate(results[1:], 1):
            # Same number of directories scanned
            assert len(result) == len(first_result), f"Scan {i} has different result count"

    def test_toctou_retry_behavior(self, temp_dir):
        """
        Test TOCTOU retry logic under concurrent access.

        SCENARIO:
        - Multiple threads checking same file
        - Another thread deleting/recreating file
        - Verify retry logic handles TOCTOU conditions
        """
        cache = FileStatCache(toctou_max_retries=3)

        # Create a test file
        test_file = os.path.join(temp_dir, "toctou_test.txt")
        with open(test_file, 'w') as f:
            f.write("TOCTOU test\n")

        errors = []
        stop_event = threading.Event()

        def deletion_worker():
            """Worker that deletes and recreates file."""
            while not stop_event.is_set():
                try:
                    if os.path.exists(test_file):
                        os.remove(test_file)
                    time.sleep(0.01)

                    with open(test_file, 'w') as f:
                        f.write("Recreated\n")
                    time.sleep(0.01)

                except Exception as e:
                    logger.debug(f"Deletion worker error: {e}")

        def stat_worker(thread_id: int):
            """Worker that stats the file."""
            try:
                for _ in range(50):
                    stat_info = cache.get_stat(test_file)
                    # Result may be None due to deletions, that's OK
                    # The important thing is no exceptions are raised

            except Exception as e:
                errors.append(f"Stat thread {thread_id}: {e}")

        # Start deletion thread
        del_thread = threading.Thread(target=deletion_worker)
        del_thread.start()

        try:
            # Run stat threads
            with ThreadPoolExecutor(max_workers=5) as executor:
                futures = [
                    executor.submit(stat_worker, i)
                    for i in range(5)
                ]

                for future in as_completed(futures):
                    future.result()

        finally:
            stop_event.set()
            del_thread.join(timeout=5.0)

        # Verify no exceptions (retries should handle TOCTOU)
        assert len(errors) == 0, f"TOCTOU errors occurred: {errors}"

    def test_lock_contention_analysis(self, test_files):
        """
        Analyze lock contention under high concurrency.

        SCENARIO:
        - Many threads accessing shared cache
        - Measure contention and performance
        """
        cache = FileStatCache(max_size=1000)
        num_threads = 20
        operations_per_thread = 100

        start_time = time.time()
        errors = []

        def cache_worker(thread_id: int):
            """Worker that performs cache operations."""
            try:
                for i in range(operations_per_thread):
                    file_path = test_files[i % len(test_files)]
                    cache.get_stat(file_path)
            except Exception as e:
                errors.append(f"Thread {thread_id}: {e}")

        # Run threads
        with ThreadPoolExecutor(max_workers=num_threads) as executor:
            futures = [
                executor.submit(cache_worker, i)
                for i in range(num_threads)
            ]

            for future in as_completed(futures):
                future.result()

        elapsed = time.time() - start_time

        # Verify no errors
        assert len(errors) == 0, f"Errors occurred: {errors}"

        # Log performance metrics
        total_operations = num_threads * operations_per_thread
        operations_per_second = total_operations / elapsed

        logger.info(
            f"Lock contention test: {total_operations} operations in {elapsed:.2f}s "
            f"({operations_per_second:.0f} ops/sec)",
            extra={'component': 'TestConcurrentIndexing', 'action': 'contention_analysis'}
        )

        # Performance should be reasonable (at least 1000 ops/sec)
        assert operations_per_second >= 1000, "Performance too low - possible lock contention"

    def test_validation_with_concurrent_access(self, temp_dir):
        """
        Test validation framework under concurrent access.

        SCENARIO:
        - Multiple threads validating paths
        - Verify validation is thread-safe
        """
        from ..validation import validate_absolute_path, validate_existing_path

        # Create test files
        test_paths = []
        for i in range(10):
            file_path = os.path.join(temp_dir, f"test_{i}.txt")
            with open(file_path, 'w') as f:
                f.write(f"Content {i}\n")
            test_paths.append(file_path)

        errors = []

        def validation_worker(thread_id: int):
            """Worker that validates paths."""
            try:
                for i in range(100):
                    path = test_paths[i % len(test_paths)]

                    # Test different validation methods
                    validated = validate_absolute_path(path)
                    assert validated == path

                    validated = validate_existing_path(path, expect_file=True)
                    assert validated == path

            except Exception as e:
                errors.append(f"Thread {thread_id}: {e}")

        # Run threads
        with ThreadPoolExecutor(max_workers=10) as executor:
            futures = [
                executor.submit(validation_worker, i)
                for i in range(10)
            ]

            for future in as_completed(futures):
                future.result()

        # Verify no errors
        assert len(errors) == 0, f"Validation errors: {errors}"


class TestSymlinkCycleDetection:
    """Test symlink cycle detection."""

    @pytest.fixture
    def symlink_dir(self):
        """Create directory structure with symlinks."""
        temp_dir = tempfile.mkdtemp()
        yield temp_dir
        shutil.rmtree(temp_dir)

    def test_symlink_cycle_detection(self, symlink_dir):
        """Test that symlink cycles are detected."""
        # Create a cycle: dir_a -> dir_b -> dir_c -> dir_a
        dir_a = os.path.join(symlink_dir, "dir_a")
        dir_b = os.path.join(symlink_dir, "dir_b")
        dir_c = os.path.join(symlink_dir, "dir_c")

        os.makedirs(dir_a)
        os.makedirs(dir_b)
        os.makedirs(dir_c)

        # Create symlink cycle
        try:
            os.symlink(dir_b, os.path.join(dir_a, "link_to_b"))
            os.symlink(dir_c, os.path.join(dir_b, "link_to_c"))
            os.symlink(dir_a, os.path.join(dir_c, "link_to_a"))
        except OSError:
            # Symlinks not supported on this system
            pytest.skip("Symlinks not supported")

        # Test scanning with symlink detection
        async def scan_with_detection():
            scanner = ParallelScanner(
                max_workers=2,
                enable_symlink_protection=True,
                max_symlink_depth=5
            )
            results = await scanner.scan(dir_a)
            return results, scanner.get_stats()

        results, stats = asyncio.run(scan_with_detection())

        # Should complete without infinite loop
        assert isinstance(results, list)
        assert stats['symlink_protection_enabled'] == True
        assert 'symlink_detector' in stats
