"""
Milestone 3: Robustness Tests

Tests for disk space checking, file size limits, and concurrent operations.
"""

from unittest.mock import MagicMock, patch

import pytest

from aperion_fsal.core import (
    MAX_FILE_SIZE,
    DiskSpaceError,
    FileSizeLimitError,
    atomic_write,
    check_disk_space,
    check_file_size,
)


class TestFileSizeLimits:
    """Tests for file size limit enforcement."""

    def test_check_file_size_within_limit(self):
        """Files within limit should pass."""
        # Should not raise
        check_file_size(1000)
        check_file_size(MAX_FILE_SIZE)

    def test_check_file_size_exceeds_limit(self):
        """Files exceeding limit should raise FileSizeLimitError."""
        with pytest.raises(FileSizeLimitError) as exc_info:
            check_file_size(MAX_FILE_SIZE + 1)
        assert "exceeds limit" in str(exc_info.value)

    def test_atomic_write_enforces_size_limit(self, tmp_path):
        """atomic_write should reject oversized content."""
        large_content = b"x" * (MAX_FILE_SIZE + 1)

        with pytest.raises(FileSizeLimitError):
            atomic_write("test.bin", large_content, root=tmp_path)

        # File should not exist
        assert not (tmp_path / "test.bin").exists()

    @pytest.mark.skip(reason="Module reload causes test isolation issues - test manually")
    def test_size_limit_configurable_via_env(self, monkeypatch, tmp_path):
        """FSAL_MAX_FILE_SIZE env var should be respected."""
        # This test reloads the module which affects other tests
        # Test manually with: FSAL_MAX_FILE_SIZE=500 python -c "..."
        pass


class TestDiskSpaceChecks:
    """Tests for disk space checking before writes."""

    def test_check_disk_space_sufficient(self, tmp_path):
        """Should pass when space is available."""
        # This should not raise (assuming test system has space)
        check_disk_space(tmp_path, 1000)

    def test_check_disk_space_insufficient(self, tmp_path, monkeypatch):
        """Should raise when space is insufficient."""

        # Mock disk_usage directly on the safeio module's shutil reference
        def mock_disk_usage(path):
            return MagicMock(
                total=100_000_000,
                used=99_000_000,
                free=1_000_000,  # Only 1MB free
            )

        # Patch directly where it's imported
        import aperion_fsal.core.safeio as safeio_module

        original = safeio_module.shutil.disk_usage
        safeio_module.shutil.disk_usage = mock_disk_usage

        try:
            # Requesting 50MB should fail (50M * 1.1 + 100M buffer = 155M needed)
            with pytest.raises(DiskSpaceError) as exc_info:
                safeio_module.check_disk_space(tmp_path, 50_000_000)
            assert "Insufficient disk space" in str(exc_info.value)
        finally:
            safeio_module.shutil.disk_usage = original

    def test_atomic_write_checks_disk_space(self, tmp_path):
        """atomic_write should check disk space before writing."""
        with patch("aperion_fsal.core.safeio.check_disk_space") as mock_check:
            mock_check.side_effect = DiskSpaceError("No space left")

            with pytest.raises(DiskSpaceError):
                atomic_write("test.txt", b"content", root=tmp_path)

            mock_check.assert_called_once()

    def test_disk_check_failure_logs_warning(self, tmp_path, monkeypatch):
        """If disk check fails (e.g., permission error), log and proceed."""

        def mock_disk_usage(path):
            raise PermissionError("No access")

        import aperion_fsal.core.safeio as safeio_module

        monkeypatch.setattr(safeio_module.shutil, "disk_usage", mock_disk_usage)

        # Should NOT raise - just log warning and proceed
        check_disk_space(tmp_path, 1000)


class TestSafeIOEdgeCases:
    """Edge cases and error handling in safe I/O operations."""

    def test_write_empty_file(self, tmp_path):
        """Empty files should be handled correctly."""
        atomic_write("empty.txt", b"", root=tmp_path)

        assert (tmp_path / "empty.txt").exists()
        assert (tmp_path / "empty.txt").read_bytes() == b""

    def test_write_preserves_parent_on_failure(self, tmp_path):
        """Parent directory should not be affected if write fails."""
        # Create parent with a file
        subdir = tmp_path / "subdir"
        subdir.mkdir()
        (subdir / "existing.txt").write_text("keep me")

        with patch("aperion_fsal.core.safeio.check_file_size") as mock:
            mock.side_effect = FileSizeLimitError("Too big")

            with pytest.raises(FileSizeLimitError):
                atomic_write("subdir/new.txt", b"content", root=tmp_path)

        # Existing file should be untouched
        assert (subdir / "existing.txt").read_text() == "keep me"

    def test_concurrent_writes_same_file(self, tmp_path):
        """Concurrent writes to same file should not corrupt."""
        import random
        import threading
        import time

        target = tmp_path / "concurrent.txt"
        results = []
        errors = []

        def writer(content: bytes, writer_id: int):
            try:
                # Small random delay to interleave
                time.sleep(random.uniform(0, 0.01))
                atomic_write("concurrent.txt", content, root=tmp_path)
                results.append(writer_id)
            except Exception as e:
                errors.append((writer_id, e))

        threads = []
        for i in range(10):
            content = f"Writer {i}\n".encode() * 100
            t = threading.Thread(target=writer, args=(content, i))
            threads.append(t)

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

        # No errors should occur
        assert len(errors) == 0, f"Errors: {errors}"

        # File should exist and be readable
        assert target.exists()
        content = target.read_text()

        # Content should be from ONE of the writers (atomic)
        assert "Writer" in content


class TestSymlinkProtection:
    """Test symlink detection in path resolution."""

    def test_symlink_in_path_blocked(self, tmp_path):
        """Symlinks in the path should be blocked by default."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        # Create directory structure
        real_dir = tmp_path / "real"
        real_dir.mkdir()
        (real_dir / "secret.txt").write_text("sensitive")

        # Create symlink inside sandbox to another dir inside sandbox
        # This should STILL be blocked by default for security
        link = tmp_path / "link"
        link.symlink_to(real_dir)

        # Trying to access through symlink should fail
        with pytest.raises(SandboxViolation) as exc_info:
            resolve_safe_path("link/secret.txt", root=tmp_path, allow_symlinks=False)
        assert "symlink" in str(exc_info.value).lower()

    def test_symlink_allowed_when_explicit(self, tmp_path):
        """Symlinks should work when allow_symlinks=True."""
        from aperion_fsal.core.sandbox import resolve_safe_path

        # Create directory structure
        real_dir = tmp_path / "real"
        real_dir.mkdir()
        (real_dir / "secret.txt").write_text("sensitive")

        # Create symlink
        link = tmp_path / "link"
        link.symlink_to(real_dir)

        # With allow_symlinks=True, should work
        result = resolve_safe_path("link/secret.txt", root=tmp_path, allow_symlinks=True)
        assert result.exists()

    def test_symlink_target_outside_sandbox(self, tmp_path):
        """Symlinks pointing outside sandbox should be blocked."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        # Create symlink to /etc
        link = tmp_path / "etc_link"
        try:
            link.symlink_to("/etc")
        except OSError:
            pytest.skip("Cannot create symlinks on this system")

        with pytest.raises(SandboxViolation):
            resolve_safe_path("etc_link/passwd", root=tmp_path)


class TestEAFPPattern:
    """Test EAFP (Easier to Ask Forgiveness than Permission) approach."""

    def test_read_nonexistent_raises(self, tmp_path):
        """Reading nonexistent file should raise FileNotFoundError."""
        from aperion_fsal.core import read

        with pytest.raises(FileNotFoundError):
            read("does_not_exist.txt", root=tmp_path)

    def test_delete_nonexistent_missing_ok(self, tmp_path):
        """Delete with missing_ok=True should not raise."""
        from aperion_fsal.core import delete

        # Should not raise
        delete("does_not_exist.txt", root=tmp_path, missing_ok=True)

    def test_mkdir_exists_ok(self, tmp_path):
        """mkdir with exist_ok=True should not raise."""
        from aperion_fsal.core import mkdir

        # Create once
        mkdir("subdir", root=tmp_path)

        # Create again with exist_ok
        mkdir("subdir", root=tmp_path, exist_ok=True)
