"""
Comprehensive Edge Case Tests for FastParallelScanner

This test module covers edge cases and failure modes identified in
SCANNER_EDGE_CASES_ANALYSIS.md. Tests are organized by category:

1. Filesystem Edge Cases
2. Concurrency Edge Cases
3. Resource Edge Cases
4. Integration Edge Cases
5. Performance Edge Cases

Run with: pytest tests/test_edge_cases.py -v
"""

import pytest
import asyncio
import os
import tempfile
import shutil
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
import time
import errno

from leindex.fast_scanner import FastParallelScanner
from leindex.parallel_scanner import SymlinkCycleDetector


# ============================================================================
# FIXTURES
# ============================================================================

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


@pytest.fixture
def scanner():
    """Create a default scanner instance."""
    return FastParallelScanner(max_workers=2)


@pytest.fixture
def large_timeout_scanner():
    """Create a scanner with large timeout to avoid premature timeouts."""
    return FastParallelScanner(max_workers=2, timeout=60.0)


# ============================================================================
# 1. FILESYSTEM EDGE CASES
# ============================================================================

class TestFilesystemEdgeCases:
    """Test filesystem-related edge cases."""

    def test_broken_symlink(self, temp_dir):
        """Test handling of broken symlinks.

        Issue: Broken symlinks are skipped silently with no indication.
        Expected: Should track and report broken symlinks.
        """
        # Create broken symlink
        broken_link = os.path.join(temp_dir, "broken_link")
        os.symlink("/nonexistent/path", broken_link)

        # Also create a valid directory
        valid_dir = os.path.join(temp_dir, "valid_dir")
        os.makedirs(valid_dir)

        scanner = FastParallelScanner(max_workers=2)
        results = scanner.scan(temp_dir)

        # Should scan valid directory
        assert len(results) == 1
        assert results[0][0] == temp_dir

        # Should have stats about skipped items (if implemented)
        stats = scanner.get_stats()
        # TODO: Implement skipped_symlinks tracking
        # assert stats.get('skipped_symlinks', 0) == 1

    def test_circular_symlink(self, temp_dir):
        """Test detection of circular symlinks.

        Issue: Circular symlinks detected but not clearly reported.
        Expected: Should detect cycle and prevent infinite loop.
        """
        # Create circular symlink structure
        dir1 = os.path.join(temp_dir, "dir1")
        dir2 = os.path.join(temp_dir, "dir2")
        os.makedirs(dir1)
        os.makedirs(dir2)

        # Create circular symlinks
        os.symlink("../dir2", os.path.join(dir1, "link_to_dir2"))
        os.symlink("../dir1", os.path.join(dir2, "link_to_dir1"))

        scanner = FastParallelScanner(max_workers=2, enable_symlink_protection=True)

        # Should complete without hanging
        start = time.time()
        results = scanner.scan(temp_dir)
        elapsed = time.time() - start

        # Should complete quickly (< 1 second)
        assert elapsed < 1.0, f"Scan took too long: {elapsed}s (possible infinite loop)"

        # Should find both directories
        dir_paths = [r[0] for r in results]
        assert dir1 in dir_paths
        assert dir2 in dir_paths

        # TODO: Check for cycle detection stats
        # stats = scanner.get_stats()
        # assert stats.get('prevented_cycles', 0) > 0

    def test_permission_denied(self, temp_dir):
        """Test handling of permission denied errors.

        Issue: Permission errors logged at DEBUG level, not tracked in stats.
        Expected: Should track permission-denied directories.
        """
        # Create directory with no permissions (on Unix)
        no_access_dir = os.path.join(temp_dir, "no_access")
        os.makedirs(no_access_dir)

        # Skip on Windows where chmod doesn't work the same way
        try:
            os.chmod(no_access_dir, 0o000)
        except (OSError, AttributeError):
            pytest.skip("Cannot test permission denied on this system")

        # Create accessible directory
        accessible_dir = os.path.join(temp_dir, "accessible")
        os.makedirs(accessible_dir)

        scanner = FastParallelScanner(max_workers=2)
        results = scanner.scan(temp_dir)

        # Should scan accessible directory
        dir_paths = [r[0] for r in results]
        assert accessible_dir in dir_paths

        # Should have stats about skipped directories (if implemented)
        stats = scanner.get_stats()
        # TODO: Implement skipped_permissions tracking
        # assert stats.get('skipped_permissions', 0) >= 1

        # Cleanup: restore permissions before temp_dir is deleted
        os.chmod(no_access_dir, 0o755)

    def test_deep_directory_structure(self, temp_dir):
        """Test handling of very deep directory structures.

        Issue: No maximum depth limit, could hit PATH_MAX.
        Expected: Should have configurable depth limit.
        """
        # Create moderately deep structure (100 levels)
        current = temp_dir
        for i in range(100):
            current = os.path.join(current, f"dir{i}")
            os.makedirs(current, exist_ok=True)

        scanner = FastParallelScanner(max_workers=2)
        results = scanner.scan(temp_dir)

        # Should scan all directories
        assert len(results) == 101  # root + 100 subdirs

        # TODO: Test depth limiting when implemented
        # scanner_limited = FastParallelScanner(max_workers=2, max_directory_depth=10)
        # with pytest.raises(ValueError, match="Max directory depth"):
        #     scanner_limited.scan(temp_dir)

    def test_special_characters_in_filename(self, temp_dir):
        """Test handling of special characters in filenames.

        Issue: Special characters could corrupt log output.
        Expected: Should sanitize or escape special chars in logs.
        """
        # Try to create files with special characters (if FS supports)
        special_names = [
            "file_with spaces",
            "file'with'quotes",
            'file"with"doublequotes',
            "file_with_parens().txt",
        ]

        for name in special_names:
            try:
                Path(os.path.join(temp_dir, name)).touch()
            except OSError:
                # Some filesystems don't support certain characters
                continue

        scanner = FastParallelScanner(max_workers=2)
        results = scanner.scan(temp_dir)

        # Should handle without crashing
        assert len(results) == 1
        _, _, files = results[0]
        # Should have created at least some files
        assert len(files) > 0

    @pytest.mark.skipif(os.name != 'posix', reason="POSIX only")
    def test_newline_in_filename(self, temp_dir):
        """Test handling of newlines in filenames.

        Issue: Newlines in filenames can corrupt log output.
        Expected: Should escape newlines in logs.
        """
        # Try to create file with newline (Linux supports this)
        try:
            filename = "file\nwith\nnewlines.txt"
            Path(os.path.join(temp_dir, filename)).touch()

            scanner = FastParallelScanner(max_workers=2)
            results = scanner.scan(temp_dir)

            # Should handle without crashing
            assert len(results) == 1
            _, _, files = results[0]
            assert filename in files

        except OSError:
            pytest.skip("Filesystem doesn't support newlines in filenames")

    def test_unicode_normalization(self, temp_dir):
        """Test handling of Unicode normalization.

        Issue: No Unicode normalization, could see same dir twice.
        Expected: Should normalize to NFC for consistency.
        """
        # Create files with Unicode characters
        unicode_names = [
            "café.txt",  # NFC
            "日本語.txt",
            "😀.txt",
            "файл.txt",  # Russian
        ]

        for name in unicode_names:
            try:
                Path(os.path.join(temp_dir, name)).touch()
            except OSError:
                # Some filesystems don't support Unicode
                continue

        scanner = FastParallelScanner(max_workers=2)
        results = scanner.scan(temp_dir)

        # Should handle without crashing
        assert len(results) == 1
        _, _, files = results[0]

        # TODO: Test normalization when implemented
        # Should not duplicate files due to normalization issues


# ============================================================================
# 2. CONCURRENCY EDGE CASES
# ============================================================================

class TestConcurrencyEdgeCases:
    """Test concurrency-related edge cases."""

    @pytest.mark.asyncio
    async def test_timeout_returns_partial_results(self, temp_dir):
        """Test that timeout returns partial results instead of raising.

        Issue: Timeout raises exception, losing all partial results.
        Expected: Should return partial results on timeout.
        """
        # Create many directories to ensure timeout
        for i in range(1000):
            d = os.path.join(temp_dir, f"dir{i}")
            os.makedirs(d, exist_ok=True)
            # Create nested structure to make it slower
            for j in range(10):
                d = os.path.join(d, f"subdir{j}")
                os.makedirs(d, exist_ok=True)

        scanner = FastParallelScanner(max_workers=2, timeout=0.5)  # Very short timeout

        # Currently raises TimeoutError
        with pytest.raises(TimeoutError):
            scanner.scan(temp_dir)

        # TODO: Should return partial results instead
        # results = scanner.scan(temp_dir)
        # assert len(results) > 0
        # assert len(results) < 11000  # Didn't complete

    @pytest.mark.asyncio
    async def test_worker_crash_detection(self, temp_dir):
        """Test detection of crashed workers.

        Issue: Worker exceptions can cause deadlock.
        Expected: Should detect and handle worker crashes.
        """
        # Create simple directory structure
        os.makedirs(os.path.join(temp_dir, "dir1"))
        os.makedirs(os.path.join(temp_dir, "dir2"))

        scanner = FastParallelScanner(max_workers=2)

        # Patch _worker to crash on second call
        original_worker = scanner._worker
        call_count = [0]

        async def crashing_worker(worker_id):
            call_count[0] += 1
            if call_count[0] == 2:
                raise RuntimeError("Simulated worker crash")
            return await original_worker(worker_id)

        scanner._worker = crashing_worker

        # Currently: Might hang or raise unclear error
        # Expected: Should detect crash and handle gracefully
        try:
            results = scanner.scan(temp_dir)
            # If it completes, results might be partial
            assert len(results) >= 0
        except Exception as e:
            # Should raise clear error about worker crash
            assert "worker" in str(e).lower() or "crash" in str(e).lower()

    def test_work_queue_validation(self, temp_dir):
        """Test validation of work queue items.

        Issue: No type checking of queue items.
        Expected: Should validate and reject invalid items.
        """
        scanner = FastParallelScanner(max_workers=2)

        # Create valid directory
        os.makedirs(os.path.join(temp_dir, "dir1"))

        # Patch to inject invalid queue item
        original_put = scanner._work_queue.put

        async def malicious_put(item):
            # Inject invalid item first
            if not hasattr(malicious_put, 'injected'):
                await scanner._work_queue.put("invalid_string")
                malicious_put.injected = True
            return await original_put(item)

        scanner._work_queue.put = malicious_put

        # Currently: Will crash when unpacking tuple
        # Expected: Should detect and skip invalid items
        with pytest.raises((ValueError, TypeError)):
            scanner.scan(temp_dir)


# ============================================================================
# 3. RESOURCE EDGE CASES
# ============================================================================

class TestResourceEdgeCases:
    """Test resource-related edge cases."""

    def test_max_workers_limit(self):
        """Test that max_workers is capped at safe value.

        Issue: No cap on max_workers, could exceed FD limit.
        Expected: Should cap at safe value (e.g., 50).
        """
        # Request very high worker count
        scanner = FastParallelScanner(max_workers=1000)

        # TODO: Should cap at safe value
        # assert scanner.max_workers <= 50

        # Currently: Accepts any value
        assert scanner.max_workers == 1000

    @pytest.mark.skip("Requires memory profiling")
    def test_memory_efficiency_large_tree(self, temp_dir):
        """Test memory usage on large directory tree.

        Issue: All results stored in memory simultaneously.
        Expected: Should provide streaming mode or document limits.
        """
        # Create 100K directories
        for i in range(100000):
            os.makedirs(os.path.join(temp_dir, f"dir{i}"), exist_ok=True)

        # TODO: Add memory profiling
        # scanner = FastParallelScanner(max_workers=4)
        # initial_memory = get_memory_usage()
        # results = scanner.scan(temp_dir)
        # final_memory = get_memory_usage()
        # assert final_memory - initial_memory < SOME_LIMIT

    def test_filesystem_unmounted(self, temp_dir):
        """Test handling when filesystem unmounted during scan.

        Issue: Poor error detection when filesystem disappears.
        Expected: Should detect ENODEV and stop scan gracefully.
        """
        # Can't easily test unmounting in unit test
        # Would require integration test with mountable filesystem

        scanner = FastParallelScanner(max_workers=2)

        # Simulate ENODEV error
        with patch('os.scandir') as mock_scandir:
            # First call succeeds, second returns ENODEV
            mock_scandir.side_effect = [
                iter([]),  # First call succeeds
                OSError(errno.ENODEV, "No such device")  # Second gets ENODEV
            ]

            # Currently: Catches as generic OSError
            # Expected: Should detect ENODEV and stop scan
            results = scanner.scan(temp_dir)
            # Behavior depends on implementation


# ============================================================================
# 4. INTEGRATION EDGE CASES
# ============================================================================

class TestIntegrationEdgeCases:
    """Test integration-related edge cases."""

    def test_ignore_matcher_none(self, temp_dir):
        """Test handling when ignore_matcher is None.

        Issue: None-handling is correct.
        Expected: Should work without ignore_matcher.
        """
        # Create directory structure
        os.makedirs(os.path.join(temp_dir, "dir1"))
        os.makedirs(os.path.join(temp_dir, "dir2"))

        scanner = FastParallelScanner(max_workers=2, ignore_matcher=None)
        results = scanner.scan(temp_dir)

        # Should scan all directories
        assert len(results) == 1
        dirs = results[0][1]
        assert len(dirs) == 2

    def test_broken_ignore_matcher(self, temp_dir):
        """Test handling of broken ignore_matcher.

        Issue: ignore_matcher exceptions crash the scanner.
        Expected: Should catch exceptions and fail-open.
        """
        # Create directory structure
        os.makedirs(os.path.join(temp_dir, "dir1"))
        os.makedirs(os.path.join(temp_dir, "dir2"))

        # Create broken ignore_matcher
        class BrokenMatcher:
            base_path = temp_dir

            def should_ignore_directory(self, path):
                raise RuntimeError("I'm broken!")

        scanner = FastParallelScanner(
            max_workers=2,
            ignore_matcher=BrokenMatcher()
        )

        # Currently: Crashes with RuntimeError
        # Expected: Should catch exception and include all directories
        with pytest.raises(RuntimeError):
            results = scanner.scan(temp_dir)

        # TODO: Should handle gracefully
        # results = scanner.scan(temp_dir)
        # assert len(results) == 1
        # assert len(results[0][1]) == 2  # Both dirs included

    def test_invalid_paths(self, temp_dir):
        """Test handling of invalid paths.

        Issue: Error messages could be more specific.
        Expected: Should validate with specific error types.
        """
        scanner = FastParallelScanner(max_workers=2)

        # Non-existent path
        with pytest.raises(OSError):
            scanner.scan("/nonexistent/path/that/does/not/exist")

        # File instead of directory
        test_file = os.path.join(temp_dir, "file.txt")
        Path(test_file).touch()

        with pytest.raises(OSError):
            scanner.scan(test_file)

        # Empty string
        with pytest.raises(OSError):
            scanner.scan("")

        # None - would cause TypeError
        with pytest.raises((OSError, TypeError)):
            scanner.scan(None)

    def test_nonexistent_directory(self, temp_dir):
        """Test handling of non-existent directory.

        Issue: Validation is correct but error could be more specific.
        Expected: Should raise FileNotFoundError.
        """
        scanner = FastParallelScanner(max_workers=2)

        # Currently raises OSError
        with pytest.raises(OSError):
            scanner.scan("/this/path/does/not/exist")

        # TODO: Should raise FileNotFoundError
        # with pytest.raises(FileNotFoundError):
        #     scanner.scan("/this/path/does/not/exist")


# ============================================================================
# 5. PERFORMANCE EDGE CASES
# ============================================================================

class TestPerformanceEdgeCases:
    """Test performance-related edge cases."""

    def test_single_file_directory(self, temp_dir):
        """Test scanning single file directory.

        Issue: Overhead of worker creation for tiny trees.
        Expected: Should optimize or document inefficiency.
        """
        # Create single empty directory
        scanner = FastParallelScanner(max_workers=4)
        results = scanner.scan(temp_dir)

        # Should work correctly
        assert len(results) == 1

        # Note: Will be slower than os.walk() for tiny trees
        # This is acceptable overhead for the performance gain on large trees

    def test_empty_directories(self, temp_dir):
        """Test scanning empty directories.

        Issue: Handled correctly.
        Expected: Should include empty directories in results.
        """
        # Create empty directory
        empty_dir = os.path.join(temp_dir, "empty")
        os.makedirs(empty_dir)

        scanner = FastParallelScanner(max_workers=2)
        results = scanner.scan(temp_dir)

        # Should include empty directory
        assert len(results) == 2  # root + empty_dir

        # Empty directory should have empty lists
        empty_result = next(r for r in results if r[0] == empty_dir)
        assert empty_result[1] == []  # No subdirs
        assert empty_result[2] == []  # No files

    @pytest.mark.skip("Creates too many files")
    def test_millions_of_files(self, temp_dir):
        """Test scanning directory with millions of files.

        Issue: Memory usage proportional to total files.
        Expected: Should provide streaming mode.
        """
        # This test is skipped by default as it creates many files
        # Run manually with: pytest tests/test_edge_cases.py::TestPerformanceEdgeCases::test_millions_of_files --skip-skip

        # Create 100K files
        for i in range(100000):
            Path(os.path.join(temp_dir, f"file{i}.txt")).touch()

        # TODO: Add memory profiling
        scanner = FastParallelScanner(max_workers=4)
        results = scanner.scan(temp_dir)

        assert len(results) == 1
        _, _, files = results[0]
        assert len(files) == 100000

    def test_very_large_directory(self, temp_dir):
        """Test scanning directory with many entries.

        Issue: Slow scandir can trigger timeout.
        Expected: Should warn about large directories.
        """
        # Create directory with 10K subdirectories
        for i in range(10000):
            d = os.path.join(temp_dir, f"dir{i}")
            os.makedirs(d, exist_ok=True)

        scanner = FastParallelScanner(
            max_workers=2,
            debug_performance=True,
            timeout=60.0
        )
        results = scanner.scan(temp_dir)

        # Should complete successfully
        assert len(results) == 10001  # root + 10K subdirs

        # Check performance stats
        stats = scanner.get_stats()
        assert stats['scanned_directories'] == 10001

        # Should have some slow scandir warnings (if > 0.1s)
        # This depends on system performance

    def test_race_condition_result_collection(self, temp_dir):
        """Test thread safety of result collection.

        Issue: Uses lock correctly.
        Expected: Should be thread-safe.
        """
        # Create many directories to increase parallelism
        for i in range(100):
            os.makedirs(os.path.join(temp_dir, f"dir{i}"), exist_ok=True)

        scanner = FastParallelScanner(max_workers=10)
        results = scanner.scan(temp_dir)

        # Should collect all results correctly
        assert len(results) == 101  # root + 100 subdirs

        # Results should be sorted
        paths = [r[0] for r in results]
        assert paths == sorted(paths)


# ============================================================================
# INTEGRATION TESTS
# ============================================================================

class TestIntegrationScenarios:
    """Integration tests for complex scenarios."""

    def test_mixed_edge_cases(self, temp_dir):
        """Test combination of multiple edge cases."""
        # Create structure with various edge cases:
        # - Normal directories
        # - Broken symlinks
        # - Circular symlinks
        # - Permission denied (if possible)
        # - Deep structure
        # - Special characters

        # Normal directories
        os.makedirs(os.path.join(temp_dir, "normal_dir"))
        os.makedirs(os.path.join(temp_dir, "dir with spaces"))
        os.makedirs(os.path.join(temp_dir, "dir'with'quotes"))

        # Broken symlink
        os.symlink("/nonexistent", os.path.join(temp_dir, "broken"))

        # Circular symlinks
        dir1 = os.path.join(temp_dir, "circle1")
        dir2 = os.path.join(temp_dir, "circle2")
        os.makedirs(dir1)
        os.makedirs(dir2)
        os.symlink("../circle2", os.path.join(dir1, "link"))
        os.symlink("../circle1", os.path.join(dir2, "link"))

        # Deep structure
        deep = temp_dir
        for i in range(20):
            deep = os.path.join(deep, f"deep{i}")
            os.makedirs(deep)

        scanner = FastParallelScanner(
            max_workers=4,
            enable_symlink_protection=True,
            timeout=30.0
        )
        results = scanner.scan(temp_dir)

        # Should complete without hanging
        assert len(results) > 0

        # Should include normal directories
        dir_paths = [r[0] for r in results]
        assert os.path.join(temp_dir, "normal_dir") in dir_paths

        # Should handle circular symlinks without infinite loop
        # (implicit: test completes in reasonable time)

    def test_concurrent_scans(self, temp_dir):
        """Test multiple concurrent scan operations."""
        # Create multiple directory trees
        roots = []
        for i in range(3):
            root = os.path.join(temp_dir, f"root{i}")
            os.makedirs(root)
            for j in range(10):
                os.makedirs(os.path.join(root, f"dir{j}"))
            roots.append(root)

        # Run scans concurrently
        async def run_concurrent():
            scanner = FastParallelScanner(max_workers=2)
            return await asyncio.to_thread(scanner.scan, roots[0])

        # Run multiple scans in parallel
        results = asyncio.run(asyncio.gather(
            asyncio.to_thread(FastParallelScanner(max_workers=2).scan, roots[0]),
            asyncio.to_thread(FastParallelScanner(max_workers=2).scan, roots[1]),
            asyncio.to_thread(FastParallelScanner(max_workers=2).scan, roots[2]),
        ))

        # All should complete successfully
        for result in results:
            assert len(result) == 11  # root + 10 subdirs


# ============================================================================
# TEST UTILITIES
# ============================================================================

def create_directory_tree(root, depth=3, breadth=3):
    """Helper to create a directory tree for testing."""
    if depth == 0:
        return

    for i in range(breadth):
        path = os.path.join(root, f"dir{i}")
        os.makedirs(path, exist_ok=True)
        create_directory_tree(path, depth - 1, breadth)


def count_files_in_tree(root):
    """Helper to count total files in directory tree."""
    count = 0
    for root, dirs, files in os.walk(root):
        count += len(files)
    return count


if __name__ == "__main__":
    # Run tests
    pytest.main([__file__, "-v", "-s"])
