"""
Security tests for path traversal vulnerabilities.

Tests all file path operations to ensure they properly prevent path traversal attacks.
"""

import os
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest


@pytest.mark.security
@pytest.mark.unit
class TestFilenameSanitization:
    """Test that filename sanitization prevents path traversal attacks."""

    def test_sanitize_removes_parent_directory_traversal(self):
        """Test that ../ patterns are removed."""
        from app.utils.filename_utils import sanitize_filename

        # Unix-style path traversal
        result = sanitize_filename("../../../etc/passwd")
        assert ".." not in result
        assert "/" not in result
        assert "etc" in result
        assert "passwd" in result

    def test_sanitize_removes_windows_path_traversal(self):
        """Test that ..\\ patterns are removed."""
        from app.utils.filename_utils import sanitize_filename

        # Windows-style path traversal
        result = sanitize_filename("..\\..\\windows\\system32")
        assert ".." not in result
        assert "\\" not in result
        assert "windows" in result

    def test_sanitize_removes_unix_path_separators(self):
        """Test that Unix path separators are removed."""
        from app.utils.filename_utils import sanitize_filename

        result = sanitize_filename("/etc/passwd")
        assert "/" not in result
        assert result == "etc_passwd"

    def test_sanitize_removes_windows_path_separators(self):
        """Test that Windows path separators are removed."""
        from app.utils.filename_utils import sanitize_filename

        result = sanitize_filename("C:\\Windows\\System32")
        assert "\\" not in result
        assert ":" not in result

    def test_sanitize_handles_mixed_path_separators(self):
        """Test that mixed path separators are handled."""
        from app.utils.filename_utils import sanitize_filename

        result = sanitize_filename("../folder/..\\..\\file.pdf")
        assert "/" not in result
        assert "\\" not in result
        assert ".." not in result

    def test_sanitize_removes_null_bytes(self):
        """Test that null bytes are removed (security issue)."""
        from app.utils.filename_utils import sanitize_filename

        result = sanitize_filename("file\x00.pdf")
        assert "\x00" not in result

    def test_sanitize_preserves_safe_characters(self):
        """Test that safe characters are preserved."""
        from app.utils.filename_utils import sanitize_filename

        result = sanitize_filename("Document_2024-01-15.pdf")
        assert result == "Document_2024-01-15.pdf"

    def test_sanitize_handles_unicode_attacks(self):
        """Test that Unicode path separators are handled."""
        from app.utils.filename_utils import sanitize_filename

        # Unicode fullwidth solidus (looks like /)
        result = sanitize_filename("folder\uff0ffile.pdf")
        # Should be replaced with underscore
        assert "\uff0f" not in result


@pytest.mark.security
@pytest.mark.unit
class TestEmbedMetadataPathTraversal:
    """Test that embed_metadata_into_pdf prevents path traversal via metadata filename."""

    def test_malicious_filename_in_metadata_is_sanitized(self, tmp_path):
        """Test that malicious filenames from GPT metadata are sanitized."""
        from app.utils.filename_utils import get_unique_filepath_with_counter, sanitize_filename

        # Simulate malicious metadata from GPT
        malicious_filename = "../../etc/passwd"

        # This should be sanitized before being used
        sanitized = sanitize_filename(malicious_filename)

        # Verify sanitization removes path traversal
        assert ".." not in sanitized
        assert "/" not in sanitized
        assert "\\" not in sanitized

        # Verify unique_filepath with sanitized name stays in directory
        result = get_unique_filepath_with_counter(str(tmp_path), sanitized, ".pdf")
        result_path = Path(result)

        # Ensure result is within tmp_path
        assert result_path.parent == tmp_path

    def test_embed_metadata_validates_filename_field(self, tmp_path):
        """Test that embed_metadata_into_pdf sanitizes the filename from metadata."""
        from app.utils.filename_utils import get_unique_filepath_with_counter, sanitize_filename

        # Test various malicious filenames
        malicious_filenames = [
            "../../../etc/passwd",
            "..\\..\\windows\\system32",
            "/etc/shadow",
            "C:\\Windows\\win.ini",
            "folder/../file",
            "folder\\..\\file",
        ]

        for malicious in malicious_filenames:
            # Sanitize as the task should do
            sanitized = sanitize_filename(malicious)

            # Verify no path traversal is possible
            result = get_unique_filepath_with_counter(str(tmp_path), sanitized, ".pdf")
            result_path = Path(result)

            # Result must be direct child of tmp_path
            assert result_path.parent == tmp_path, f"Failed for: {malicious}"

    @patch("app.tasks.embed_metadata_into_pdf.settings")
    @patch("app.tasks.embed_metadata_into_pdf.SessionLocal")
    @patch("app.tasks.embed_metadata_into_pdf.log_task_progress")
    @patch("app.tasks.embed_metadata_into_pdf.finalize_document_storage")
    @patch("pypdf.PdfReader")
    @patch("pypdf.PdfWriter")
    def test_embed_metadata_full_flow_with_malicious_filename(
        self,
        mock_pdf_writer,
        mock_pdf_reader,
        mock_finalize,
        mock_log,
        mock_session,
        mock_settings,
        tmp_path,
    ):
        """Integration test: full embed_metadata_into_pdf with malicious filename."""
        from app.tasks.embed_metadata_into_pdf import embed_metadata_into_pdf

        # Setup
        mock_settings.workdir = str(tmp_path)

        # Create a temporary PDF file
        test_pdf = tmp_path / "test.pdf"
        test_pdf.write_bytes(b"%PDF-1.4\n")

        # Mock PDF operations
        mock_reader_instance = MagicMock()
        mock_reader_instance.pages = []
        mock_pdf_reader.return_value = mock_reader_instance

        mock_writer_instance = MagicMock()
        mock_pdf_writer.return_value = mock_writer_instance

        # Malicious metadata from GPT
        malicious_metadata = {
            "filename": "../../../etc/passwd",  # Path traversal attempt
            "document_type": "Invoice",
            "tags": ["test"],
        }

        # Create processed directory
        processed_dir = tmp_path / "processed"
        processed_dir.mkdir()

        # Execute task (called directly, Celery injects 'self' automatically)
        result = embed_metadata_into_pdf(
            str(test_pdf),
            "test text",
            malicious_metadata,
            file_id=1,
        )

        # Verify the result file is in the processed directory
        # and not in /etc/ or parent directories
        if "file" in result:
            result_path = Path(result["file"])
            # Should be in processed directory
            assert result_path.parent == processed_dir
            # Should not contain path traversal
            assert ".." not in result_path.name
            assert "/" not in result_path.name
            assert "\\" not in result_path.name


@pytest.mark.security
@pytest.mark.unit
class TestExtractMetadataFilenameValidation:
    """Test that extract_metadata_with_gpt validates GPT-provided filenames."""

    def test_validates_filename_format(self):
        """Test that invalid filename formats are rejected."""
        import re

        from app.utils.filename_utils import VALID_FILENAME_PATTERN

        # Valid pattern from app.utils.filename_utils
        valid_pattern = VALID_FILENAME_PATTERN

        # Test valid filenames
        valid_filenames = [
            "2024-01-15_Invoice.pdf",
            "Document_Name.pdf",
            "My Document 2024.pdf",
            "file-name_123.pdf",
        ]

        for filename in valid_filenames:
            # Remove extension for test
            name_only = filename.rsplit(".", 1)[0]
            assert re.match(valid_pattern, name_only), f"Valid filename rejected: {filename}"

        # Test invalid filenames
        invalid_filenames = [
            "../../../etc/passwd",
            "/etc/shadow",
            "C:\\Windows\\system32",
            "folder/../file",
            "file:name.pdf",
            "file|name.pdf",
            "file<>name.pdf",
        ]

        for filename in invalid_filenames:
            assert not re.match(valid_pattern, filename), f"Invalid filename accepted: {filename}"

    def test_rejects_path_traversal_in_filename(self):
        """Test that path traversal patterns are detected."""
        malicious_filenames = [
            "../../../etc/passwd",
            "..\\..\\windows\\system32",
            "/etc/shadow",
            "folder/../file",
        ]

        for filename in malicious_filenames:
            # Check for path traversal indicators
            has_traversal = ".." in filename or "/" in filename or "\\" in filename
            assert has_traversal, f"Path traversal not detected: {filename}"


@pytest.mark.security
@pytest.mark.unit
class TestPathValidationSecurity:
    """Test secure path validation using pathlib."""

    def test_is_relative_to_prevents_traversal(self, tmp_path):
        """Test that is_relative_to prevents directory traversal."""
        base_dir = tmp_path / "workdir"
        base_dir.mkdir()

        # Create a file outside base_dir
        outside_dir = tmp_path / "outside"
        outside_dir.mkdir()
        outside_file = outside_dir / "file.txt"
        outside_file.write_text("test")

        # Attempt to access file outside base_dir
        try:
            outside_resolved = outside_file.resolve()
            base_resolved = base_dir.resolve()

            # Should return False (file is not relative to base_dir)
            is_safe = outside_resolved.is_relative_to(base_resolved)
            assert not is_safe, "Path traversal not detected"
        except ValueError:
            # In some Python versions, is_relative_to may raise ValueError
            # This is also acceptable (indicates not relative)
            pass

    def test_resolve_prevents_symlink_attacks(self, tmp_path):
        """Test that resolve() handles symlink attacks."""
        base_dir = tmp_path / "workdir"
        base_dir.mkdir()

        # Create target outside base_dir
        outside_dir = tmp_path / "outside"
        outside_dir.mkdir()
        target_file = outside_dir / "secret.txt"
        target_file.write_text("secret")

        # Create symlink inside base_dir pointing outside
        symlink_path = base_dir / "link.txt"
        symlink_path.symlink_to(target_file)

        # Resolve should give us the real path
        resolved = symlink_path.resolve()
        base_resolved = base_dir.resolve()

        # The resolved path should NOT be relative to base_dir
        try:
            is_safe = resolved.is_relative_to(base_resolved)
            assert not is_safe, "Symlink attack not detected"
        except ValueError:
            # is_relative_to raises ValueError if not relative
            pass

    def test_string_based_validation_is_insecure(self, tmp_path):
        """Demonstrate why string-based path validation is insecure."""
        base_dir = tmp_path / "workdir"
        base_dir.mkdir()

        # Create a similar-named directory
        fake_dir = tmp_path / "workdir-fake"
        fake_dir.mkdir()
        fake_file = fake_dir / "file.txt"
        fake_file.write_text("content")

        # String-based check (insecure)
        base_str = str(base_dir)
        fake_str = str(fake_file)

        # This would INCORRECTLY pass string.startswith() if not careful
        # because "workdir-fake" starts with "workdir"
        if base_str.endswith("/") or base_str.endswith("\\"):
            # Properly add separator
            string_check_unsafe = fake_str.startswith(base_str)
        else:
            # Without separator, vulnerable to partial matches
            string_check_unsafe = fake_str.startswith(base_str)

        # Pathlib-based check (secure)
        try:
            pathlib_check = fake_file.resolve().is_relative_to(base_dir.resolve())
            # Should correctly identify this is NOT under base_dir
            assert not pathlib_check, "Pathlib should reject this path"
        except ValueError:
            # Correctly rejected
            pass


@pytest.mark.security
@pytest.mark.unit
class TestFileUploadSecurity:
    """Test that file upload endpoints prevent path traversal."""

    def test_ui_upload_uses_basename(self):
        """Test that ui_upload extracts basename to prevent path traversal."""

        from app.utils.filename_utils import sanitize_filename

        # Simulate malicious filenames
        malicious_filenames = [
            "../../../etc/passwd",
            "/etc/shadow",
            "folder/../file.pdf",
        ]

        for malicious in malicious_filenames:
            # os.path.basename should extract just the filename
            basename = os.path.basename(malicious)

            # Verify no path traversal remains in basename
            assert ".." not in basename, f"Path traversal not removed: {malicious} -> {basename}"
            assert "/" not in basename, f"Path separator not removed: {malicious} -> {basename}"

        # Windows-style backslash paths: os.path.basename on Linux does NOT
        # split on backslash, so the application also uses sanitize_filename
        # to handle these.  Verify the combined approach is safe.
        windows_paths = [
            "..\\..\\windows\\system32",
        ]
        for malicious in windows_paths:
            sanitized = sanitize_filename(os.path.basename(malicious))
            assert ".." not in sanitized, f"Path traversal not removed after sanitize: {malicious} -> {sanitized}"
            assert "\\" not in sanitized, f"Backslash not removed after sanitize: {malicious} -> {sanitized}"

    def test_sanitize_after_basename(self):
        """Test that sanitization happens after basename extraction."""

        from app.utils.filename_utils import sanitize_filename

        malicious = "../../../passwd.pdf"

        # Step 1: Extract basename (as ui_upload does)
        basename = os.path.basename(malicious)
        assert basename == "passwd.pdf"

        # Step 2: Sanitize (as ui_upload does)
        sanitized = sanitize_filename(basename)
        assert sanitized == "passwd.pdf"

        # Final result is safe
        assert ".." not in sanitized
        assert "/" not in sanitized


@pytest.mark.security
@pytest.mark.unit
class TestFileHashSecurity:
    """Test that file hashing doesn't introduce vulnerabilities."""

    def test_hash_file_with_absolute_path_only(self, tmp_path):
        """Test that hash_file should only accept absolute paths."""
        from app.utils.file_operations import hash_file

        # Create a test file
        test_file = tmp_path / "test.pdf"
        test_file.write_bytes(b"test content")

        # Should work with absolute path
        result = hash_file(str(test_file))
        assert isinstance(result, str)
        assert len(result) == 64  # SHA-256 hex digest length

    def test_hash_file_rejects_path_traversal(self):
        """Test that hash_file doesn't allow path traversal."""
        from app.utils.file_operations import hash_file

        # Attempt to hash a file using path traversal
        # This should fail because the file doesn't exist
        with pytest.raises(FileNotFoundError):
            hash_file("../../../etc/passwd")


@pytest.mark.security
@pytest.mark.integration
class TestEndToEndPathTraversal:
    """Integration tests for path traversal prevention."""

    def test_full_upload_flow_prevents_traversal(self, tmp_path):
        """Test complete upload flow prevents path traversal."""
        import uuid

        from app.utils.filename_utils import sanitize_filename

        # Simulate ui_upload flow
        malicious_upload_filename = "../../../etc/passwd"

        # Step 1: Extract basename
        base_filename = os.path.basename(malicious_upload_filename)
        assert base_filename == "passwd"

        # Step 2: Sanitize
        safe_filename = sanitize_filename(base_filename)
        assert safe_filename == "passwd"

        # Step 3: Add UUID (as ui_upload does)
        unique_id = str(uuid.uuid4())
        target_filename = f"{unique_id}.{safe_filename}"

        # Step 4: Join with workdir
        target_path = os.path.join(str(tmp_path), target_filename)

        # Verify final path is safe
        final_path = Path(target_path)
        assert final_path.parent == tmp_path
        assert ".." not in target_filename
        assert "/" not in target_filename

    def test_metadata_embedding_flow_prevents_traversal(self, tmp_path):
        """Test metadata embedding flow prevents path traversal."""

        from app.utils.filename_utils import sanitize_filename

        # Simulate GPT returning malicious filename
        gpt_metadata = {
            "filename": "../../../etc/shadow",
            "document_type": "Invoice",
        }

        # Step 1: Extract filename from metadata
        suggested_filename = gpt_metadata.get("filename", "fallback")

        # Step 2: Sanitize (as embed_metadata_into_pdf should do)
        suggested_filename = sanitize_filename(suggested_filename)

        # Step 3: Remove extension
        suggested_filename = os.path.splitext(suggested_filename)[0]

        # Step 4: Build final path
        processed_dir = tmp_path / "processed"
        processed_dir.mkdir()
        final_path = os.path.join(str(processed_dir), f"{suggested_filename}.pdf")

        # Verify final path is safe
        result_path = Path(final_path)
        assert result_path.parent == processed_dir
        assert ".." not in str(result_path)
