"""
Tests for FileUploadValidator security module.
"""

from unittest.mock import Mock, patch

from local_deep_research.security.file_upload_validator import (
    FileUploadValidator,
)


class TestFileUploadValidatorConstants:
    """Tests for FileUploadValidator constants."""

    def test_max_file_size_defined(self):
        """MAX_FILE_SIZE is defined and matches the 3 GB server-config default."""
        assert FileUploadValidator.MAX_FILE_SIZE > 0
        assert FileUploadValidator.MAX_FILE_SIZE == 3 * 1024 * 1024 * 1024

    def test_max_files_per_request_defined(self):
        """MAX_FILES_PER_REQUEST is defined and reasonable."""
        assert FileUploadValidator.MAX_FILES_PER_REQUEST > 0
        assert FileUploadValidator.MAX_FILES_PER_REQUEST == 200

    def test_pdf_magic_bytes_correct(self):
        """PDF_MAGIC_BYTES is correct."""
        assert FileUploadValidator.PDF_MAGIC_BYTES == b"%PDF"

    def test_allowed_mime_types_includes_pdf(self):
        """ALLOWED_MIME_TYPES includes PDF."""
        assert "application/pdf" in FileUploadValidator.ALLOWED_MIME_TYPES


class TestResolveMaxFileSize:
    """The per-file cap honors LDR_SECURITY_UPLOAD_MAX_FILE_SIZE_MB."""

    def test_default_when_env_unset(self):
        from local_deep_research.security import file_upload_validator as mod

        with patch.object(mod, "check_env_setting", return_value=None):
            assert (
                mod._resolve_max_file_size()
                == mod._DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024
            )

    def test_env_value_overrides_default(self):
        from local_deep_research.security import file_upload_validator as mod

        # 750MB override (env vars always arrive as strings).
        with patch.object(mod, "check_env_setting", return_value="750"):
            assert mod._resolve_max_file_size() == 750 * 1024 * 1024

    def test_unparseable_env_falls_back_to_default(self):
        from local_deep_research.security import file_upload_validator as mod

        with patch.object(
            mod, "check_env_setting", return_value="not-a-number"
        ):
            assert (
                mod._resolve_max_file_size()
                == mod._DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024
            )

    def test_zero_env_value_falls_back_to_default(self):
        """A misconfigured cap of 0 would silently break all uploads."""
        from local_deep_research.security import file_upload_validator as mod

        with patch.object(mod, "check_env_setting", return_value="0"):
            assert (
                mod._resolve_max_file_size()
                == mod._DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024
            )

    def test_negative_env_value_falls_back_to_default(self):
        """Negative values are nonsensical for a size cap — reject them."""
        from local_deep_research.security import file_upload_validator as mod

        with patch.object(mod, "check_env_setting", return_value="-1"):
            assert (
                mod._resolve_max_file_size()
                == mod._DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024
            )


class TestValidateFileSize:
    """Tests for FileUploadValidator.validate_file_size()."""

    def test_valid_content_length(self):
        """Accepts valid content length."""
        is_valid, error = FileUploadValidator.validate_file_size(1000)
        assert is_valid is True
        assert error is None

    def test_valid_file_content(self):
        """Accepts valid file content size."""
        content = b"x" * 1000
        is_valid, error = FileUploadValidator.validate_file_size(None, content)
        assert is_valid is True
        assert error is None

    def test_content_length_exceeds_max(self):
        """Rejects content length exceeding max."""
        large_size = FileUploadValidator.MAX_FILE_SIZE + 1
        is_valid, error = FileUploadValidator.validate_file_size(large_size)
        assert is_valid is False
        assert "too large" in error.lower()

    def test_file_content_exceeds_max(self):
        """Rejects file content exceeding max.

        Patches the cap to a small value so the test doesn't have to
        allocate gigabytes to cross the real default.
        """
        with patch.object(FileUploadValidator, "MAX_FILE_SIZE", 1024):
            large_content = b"x" * 2048  # 2KB over a 1KB cap
            is_valid, error = FileUploadValidator.validate_file_size(
                None, large_content
            )
        assert is_valid is False
        assert "too large" in error.lower()

    def test_both_none_is_valid(self):
        """Accepts when both parameters are None."""
        is_valid, error = FileUploadValidator.validate_file_size(None, None)
        assert is_valid is True
        assert error is None

    def test_zero_content_length_is_valid(self):
        """Accepts zero content length."""
        is_valid, error = FileUploadValidator.validate_file_size(0)
        assert is_valid is True
        assert error is None

    def test_error_message_includes_size(self):
        """Error message includes file size information."""
        with patch.object(
            FileUploadValidator, "MAX_FILE_SIZE", 50 * 1024 * 1024
        ):
            large_size = 60 * 1024 * 1024  # 60MB, over a patched 50MB cap
            is_valid, error = FileUploadValidator.validate_file_size(large_size)
        assert is_valid is False
        assert "60" in error or "MB" in error


class TestValidateFileCount:
    """Tests for FileUploadValidator.validate_file_count()."""

    def test_valid_file_count(self):
        """Accepts valid file count."""
        is_valid, error = FileUploadValidator.validate_file_count(10)
        assert is_valid is True
        assert error is None

    def test_single_file_valid(self):
        """Accepts single file."""
        is_valid, error = FileUploadValidator.validate_file_count(1)
        assert is_valid is True
        assert error is None

    def test_max_files_valid(self):
        """Accepts exactly max files."""
        is_valid, error = FileUploadValidator.validate_file_count(
            FileUploadValidator.MAX_FILES_PER_REQUEST
        )
        assert is_valid is True
        assert error is None

    def test_exceeds_max_files(self):
        """Rejects count exceeding max."""
        is_valid, error = FileUploadValidator.validate_file_count(
            FileUploadValidator.MAX_FILES_PER_REQUEST + 1
        )
        assert is_valid is False
        assert "too many" in error.lower()

    def test_zero_files_invalid(self):
        """Rejects zero files."""
        is_valid, error = FileUploadValidator.validate_file_count(0)
        assert is_valid is False
        assert "no files" in error.lower()

    def test_negative_files_invalid(self):
        """Rejects negative file count."""
        is_valid, error = FileUploadValidator.validate_file_count(-1)
        assert is_valid is False
        assert "no files" in error.lower()


class TestValidateMimeType:
    """Tests for FileUploadValidator.validate_mime_type()."""

    def test_valid_pdf(self):
        """Accepts valid PDF file."""
        content = b"%PDF-1.4 test content"
        is_valid, error = FileUploadValidator.validate_mime_type(
            "document.pdf", content
        )
        assert is_valid is True
        assert error is None

    def test_valid_pdf_uppercase_extension(self):
        """Accepts PDF with uppercase extension."""
        content = b"%PDF-1.4 test content"
        is_valid, error = FileUploadValidator.validate_mime_type(
            "document.PDF", content
        )
        assert is_valid is True
        assert error is None

    def test_invalid_extension(self):
        """Rejects non-PDF extension."""
        content = b"%PDF-1.4 test content"
        is_valid, error = FileUploadValidator.validate_mime_type(
            "document.txt", content
        )
        assert is_valid is False
        assert "only pdf" in error.lower()

    def test_invalid_magic_bytes(self):
        """Rejects file with wrong magic bytes."""
        content = b"PK\x03\x04"  # ZIP magic bytes
        is_valid, error = FileUploadValidator.validate_mime_type(
            "document.pdf", content
        )
        assert is_valid is False
        assert "signature" in error.lower()

    def test_empty_content(self):
        """Rejects empty file content."""
        is_valid, error = FileUploadValidator.validate_mime_type(
            "document.pdf", b""
        )
        assert is_valid is False
        assert "signature" in error.lower()

    def test_too_short_content(self):
        """Rejects content shorter than magic bytes."""
        is_valid, error = FileUploadValidator.validate_mime_type(
            "document.pdf", b"%PD"
        )
        assert is_valid is False
        assert "signature" in error.lower()


class TestValidatePdfStructure:
    """Tests for FileUploadValidator.validate_pdf_structure()."""

    def test_valid_pdf_structure(self, mock_pdf_content):
        """Accepts valid PDF structure."""
        with patch(
            "local_deep_research.security.file_upload_validator.pdfplumber"
        ) as mock_pdfplumber:
            mock_pdf = Mock()
            mock_page = Mock()
            mock_page.width = 612
            mock_page.height = 792
            mock_pdf.pages = [mock_page]
            mock_pdfplumber.open.return_value.__enter__ = Mock(
                return_value=mock_pdf
            )
            mock_pdfplumber.open.return_value.__exit__ = Mock(
                return_value=False
            )

            is_valid, error = FileUploadValidator.validate_pdf_structure(
                "document.pdf", mock_pdf_content
            )
            assert is_valid is True
            assert error is None

    def test_pdf_no_pages(self, mock_pdf_content):
        """Rejects PDF with no pages."""
        with patch(
            "local_deep_research.security.file_upload_validator.pdfplumber"
        ) as mock_pdfplumber:
            mock_pdf = Mock()
            mock_pdf.pages = []
            mock_pdfplumber.open.return_value.__enter__ = Mock(
                return_value=mock_pdf
            )
            mock_pdfplumber.open.return_value.__exit__ = Mock(
                return_value=False
            )

            is_valid, error = FileUploadValidator.validate_pdf_structure(
                "document.pdf", mock_pdf_content
            )
            assert is_valid is False
            assert "no pages" in error.lower()

    def test_pdf_parse_error(self, mock_pdf_content):
        """Rejects PDF that cannot be parsed."""
        with patch(
            "local_deep_research.security.file_upload_validator.pdfplumber"
        ) as mock_pdfplumber:
            mock_pdfplumber.open.side_effect = Exception("PDF parsing error")

            is_valid, error = FileUploadValidator.validate_pdf_structure(
                "corrupt.pdf", mock_pdf_content
            )
            assert is_valid is False
            assert "invalid" in error.lower() or "corrupted" in error.lower()

    def test_pdf_none_pages(self, mock_pdf_content):
        """Rejects PDF with None pages."""
        with patch(
            "local_deep_research.security.file_upload_validator.pdfplumber"
        ) as mock_pdfplumber:
            mock_pdf = Mock()
            mock_pdf.pages = None
            mock_pdfplumber.open.return_value.__enter__ = Mock(
                return_value=mock_pdf
            )
            mock_pdfplumber.open.return_value.__exit__ = Mock(
                return_value=False
            )

            is_valid, error = FileUploadValidator.validate_pdf_structure(
                "document.pdf", mock_pdf_content
            )
            assert is_valid is False
            assert "no pages" in error.lower()


class TestValidateUpload:
    """Tests for FileUploadValidator.validate_upload()."""

    def test_valid_upload(self, mock_pdf_content):
        """Accepts valid file upload."""
        with patch(
            "local_deep_research.security.file_upload_validator.pdfplumber"
        ) as mock_pdfplumber:
            mock_pdf = Mock()
            mock_page = Mock()
            mock_page.width = 612
            mock_page.height = 792
            mock_pdf.pages = [mock_page]
            mock_pdfplumber.open.return_value.__enter__ = Mock(
                return_value=mock_pdf
            )
            mock_pdfplumber.open.return_value.__exit__ = Mock(
                return_value=False
            )

            is_valid, error = FileUploadValidator.validate_upload(
                "document.pdf", mock_pdf_content
            )
            assert is_valid is True
            assert error is None

    def test_upload_file_too_large(self):
        """Rejects upload that's too large."""
        with patch.object(FileUploadValidator, "MAX_FILE_SIZE", 1024):
            large_content = b"%PDF-1.4" + b"x" * 2048  # 2KB over 1KB cap
            is_valid, error = FileUploadValidator.validate_upload(
                "document.pdf", large_content
            )
        assert is_valid is False
        assert "too large" in error.lower()

    def test_upload_wrong_extension(self, mock_pdf_content):
        """Rejects upload with wrong extension."""
        is_valid, error = FileUploadValidator.validate_upload(
            "document.txt", mock_pdf_content
        )
        assert is_valid is False
        assert "only pdf" in error.lower()

    def test_upload_wrong_magic_bytes(self):
        """Rejects upload with wrong magic bytes."""
        content = b"not a pdf"
        is_valid, error = FileUploadValidator.validate_upload(
            "document.pdf", content
        )
        assert is_valid is False
        assert "signature" in error.lower()

    def test_upload_corrupted_pdf(self, mock_pdf_content):
        """Rejects corrupted PDF."""
        with patch(
            "local_deep_research.security.file_upload_validator.pdfplumber"
        ) as mock_pdfplumber:
            mock_pdfplumber.open.side_effect = Exception("Corrupted")

            is_valid, error = FileUploadValidator.validate_upload(
                "document.pdf", mock_pdf_content
            )
            assert is_valid is False
            assert "invalid" in error.lower() or "corrupted" in error.lower()

    def test_upload_with_content_length(self, mock_pdf_content):
        """Validates with content length header."""
        with patch(
            "local_deep_research.security.file_upload_validator.pdfplumber"
        ) as mock_pdfplumber:
            mock_pdf = Mock()
            mock_page = Mock()
            mock_page.width = 612
            mock_page.height = 792
            mock_pdf.pages = [mock_page]
            mock_pdfplumber.open.return_value.__enter__ = Mock(
                return_value=mock_pdf
            )
            mock_pdfplumber.open.return_value.__exit__ = Mock(
                return_value=False
            )

            is_valid, error = FileUploadValidator.validate_upload(
                "document.pdf",
                mock_pdf_content,
                content_length=len(mock_pdf_content),
            )
            assert is_valid is True
            assert error is None

    def test_upload_content_length_mismatch_large(self, mock_pdf_content):
        """Rejects when content length indicates too large."""
        # Content length header says file is too large
        is_valid, error = FileUploadValidator.validate_upload(
            "document.pdf",
            mock_pdf_content,
            content_length=FileUploadValidator.MAX_FILE_SIZE + 1,
        )
        assert is_valid is False
        assert "too large" in error.lower()


class TestSecurityScenarios:
    """Integration tests for security scenarios."""

    def test_disguised_executable(self):
        """Rejects executable disguised as PDF."""
        # Windows executable magic bytes
        content = b"MZ" + b"\x00" * 100
        is_valid, error = FileUploadValidator.validate_upload(
            "malware.pdf", content
        )
        assert is_valid is False

    def test_zip_bomb_prevention(self):
        """Rejects suspiciously large claimed size."""
        # Content length claims enormous size
        content = b"%PDF-1.4 small content"
        is_valid, error = FileUploadValidator.validate_upload(
            "bomb.pdf",
            content,
            content_length=10 * 1024 * 1024 * 1024,  # 10GB
        )
        assert is_valid is False

    def test_polyglot_file_rejection(self):
        """Rejects files that start with valid PDF but have wrong extension."""
        content = b"%PDF-1.4 this is actually executable code"
        is_valid, error = FileUploadValidator.validate_upload(
            "script.js", content
        )
        assert is_valid is False

    def test_html_in_pdf_extension(self):
        """Rejects HTML file with PDF extension."""
        content = b"<html><script>alert('xss')</script></html>"
        is_valid, error = FileUploadValidator.validate_upload(
            "attack.pdf", content
        )
        assert is_valid is False
        assert "signature" in error.lower()
